From 122ab9e0be794408d9ec6e941c109a91957df77e Mon Sep 17 00:00:00 2001 From: asymtote <152078151+asymtote@users.noreply.github.com> Date: Fri, 24 Jul 2026 01:52:23 +0530 Subject: [PATCH] Add cpp_supervised_app_simple example for the Alive API (#234) Adds a minimal example showcasing score::mw::lifecycle::Alive, as requested: construct Alive, report Running, then ReportAlive() in a loop until SIGTERM/SIGINT, with no deadline/checkpoint supervision (that's what distinguishes it from the existing cpp_supervised_app, which uses the HealthMonitor/DeadlineMonitor API instead). - examples/cpp_supervised_app_simple/main.cpp + BUILD: modeled closely on cpp_supervised_app's structure (signal handling, set_process_name, getopt-based CLI with -s ), swapping the HealthMonitor block for score::mw::lifecycle::Alive + report_running(). Depends on the //score/launch_manager:alive_cc and :lifecycle_cc aliases (same public-alias pattern the existing examples already use, since the underlying alive/lifecycle_client targets have restricted visibility). Alive construction can throw per its documented contract, so it's wrapped in try/catch for a clean error message instead of an unhandled-exception terminate(). - Registered as a new "cpp_supervised_app_simple" component in examples/demo_verification/lifecycle_demo_test.json, added to the Running run target's depends_on. Left application_type unset (defaults to "Reporting", same as lifecycle_app) since this app only demonstrates the alive heartbeat, not deadline-checkpoint supervision -- happy to change to Reporting_And_Supervised if a maintainer prefers that classification. - Added the new binary to demo_verification/BUILD's integration_test binaries list and to test_examples.py's _DEMO_APPS. - Documented the new app in examples/README.md's app table. Found and fixed a real bug while wiring up the demo config: this app's name is a literal prefix-superstring of the existing "cpp_supervised_app", and test_examples.py's process-matching was a plain unanchored substring grep over /proc/*/cmdline. That means grep -qa 'cpp_supervised_app' would also match "cpp_supervised_app_simple"'s cmdline -- breaking _assert_not_running(target, "cpp_supervised_app") after the existing SIGKILL crash-recovery step (it would spuriously find the still-alive _simple process and report the killed app as still running). Verified this concretely by simulating /proc/*/cmdline-style files locally (NUL-separated argv, both bare-name and full-path forms) and reproducing the false match, then fixing test_examples.py's _assert_running/_assert_not_running/_send_signal to convert cmdline's NUL-separated argv into one-token-per-line and anchor the match (^binary$ or ^.*/binary$) instead of doing a raw substring search -- re-ran the same simulation against the fixed logic and confirmed it correctly distinguishes the two names, still matches full-path argv0, and doesn't self-match the invoking shell's own command line (the anchoring incidentally subsumes the original bracket-class self-match-avoidance trick, so I dropped it). Verification note: this repo requires Bazel, which isn't available in my environment, so I could not build or run the integration test. Everything above was verified by reading the actual headers (score/launch_manager/src/alive/src/alive.h, .../lifecycle_client/src/report_running.h) and BUILD files to confirm target names, include paths, and visibility, by closely mirroring the existing cpp_supervised_app example, and by empirically simulating the test_examples.py matching logic outside Bazel as described above. CI should be treated as the real verification here. --- examples/README.md | 1 + examples/cpp_supervised_app_simple/BUILD | 38 ++++++ examples/cpp_supervised_app_simple/main.cpp | 126 ++++++++++++++++++ examples/demo_verification/BUILD | 1 + .../lifecycle_demo_test.json | 17 ++- examples/demo_verification/test_examples.py | 39 ++++-- 6 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 examples/cpp_supervised_app_simple/BUILD create mode 100644 examples/cpp_supervised_app_simple/main.cpp 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}"