diff --git a/score/mw/com/test/common_test_resources/BUILD b/score/mw/com/test/common_test_resources/BUILD index 8b96b95b8..6347f978a 100644 --- a/score/mw/com/test/common_test_resources/BUILD +++ b/score/mw/com/test/common_test_resources/BUILD @@ -362,6 +362,7 @@ cc_library( srcs = ["general_resources.cpp"], hdrs = ["general_resources.h"], features = COMPILER_WARNING_FEATURES, + visibility = ["//score/mw/com/test:__subpackages__"], deps = [ ":check_point_control", ":child_process_guard", diff --git a/score/mw/com/test/inotify_stress_test/BUILD b/score/mw/com/test/inotify_stress_test/BUILD new file mode 100644 index 000000000..6a1bd6d01 --- /dev/null +++ b/score/mw/com/test/inotify_stress_test/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:defs.bzl", "cc_binary") +load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES") +load("//score/mw/com/test:pkg_application.bzl", "pkg_application") + +cc_binary( + name = "inotify_stress_test", + srcs = [ + "controller.cpp", + "controller.h", + "inotify_stress_test.cpp", + "inotify_stress_test_internal.h", + "worker.cpp", + "worker.h", + ], + features = COMPILER_WARNING_FEATURES + [ + "aborts_upon_exception", + ], + visibility = ["//score/mw/com/test/inotify_stress_test:__subpackages__"], + deps = [ + "//score/mw/com/test/common_test_resources:check_point_control", + "//score/mw/com/test/common_test_resources:general_resources", + "@boost.program_options", + "@score_baselibs//score/filesystem", + "@score_baselibs//score/language/futurecpp", + "@score_baselibs//score/mw/log", + "@score_baselibs//score/os/utils/inotify:inotify_instance_impl", + ], +) + +pkg_application( + name = "inotify-stress-test-pkg", + app_name = "InotifyStressTestApp", + bin = [":inotify_stress_test"], + visibility = [ + "//score/mw/com/test/inotify_stress_test:__subpackages__", + ], +) diff --git a/score/mw/com/test/inotify_stress_test/README.md b/score/mw/com/test/inotify_stress_test/README.md new file mode 100644 index 000000000..f6b3d2e2e --- /dev/null +++ b/score/mw/com/test/inotify_stress_test/README.md @@ -0,0 +1,63 @@ +# inotify Stress Test + +Stress test that exercises the `os::InotifyInstanceImpl` inotify wrapper under heavy +concurrent load from multiple processes. + +## What it does + +A controller process spawns `M` worker processes. Each worker owns a dedicated +sub-directory and file under a shared base folder and is optionally assigned a unique +UID/GID. The controller and workers synchronise through `CheckPointControl` objects held +in shared memory, so every cycle is driven in lock-step. + +On each of the `N` cycles a worker: + +1. Creates its process directory (skipped when it already exists with the correct + access rights). +2. Creates its test file (skipped when it already exists with the correct access rights). +3. Adds an inotify watch on the shared base folder + (`kInCreate | kInDelete`). +4. Removes the watch immediately. +5. Reports the cycle as reached via `CheckPointReached`. + +The controller signals each worker to start a cycle (`ProceedToNextCheckpoint`), waits for +every worker to report success or error (`WaitAndVerifyCheckPoint`), removes the per-process +directories so the next cycle re-exercises the creation path, and after all cycles sends +`FinishActions` so workers exit cleanly. Any worker failure fails the whole test. + +## Arguments + +| Argument | Default | Description | +| ----------------- | ------- | ------------------------------------------------------------------ | +| `--num-processes` | 5 | Number of worker processes to spawn. | +| `--cycles` | 100 | Number of stress cycles before terminating. | +| `--base-uid` | 0 | Worker `N` calls `setuid(base-uid + N)`; `0` skips `setuid`. | +| `--base-gid` | 0 | Worker `N` calls `setgid(base-gid + N)`; `0` skips `setgid`. | + +When `--base-uid` / `--base-gid` are non-zero the setuid/setgid must succeed, so the binary +has to run as root (e.g. inside the Docker integration-test container). Local non-root runs +should leave these at their default `0`. + +## Layout + +| File | Responsibility | +| -------------------------------- | ----------------------------------------------------------- | +| `inotify_stress_test.cpp` | Entry point: argument parsing, setup, fork orchestration. | +| `worker.{h,cpp}` | Per-worker cycle loop and UID/GID credential switching. | +| `controller.{h,cpp}` | Controller loop: signals workers, verifies checkpoints. | +| `inotify_stress_test_internal.h` | Shared constants and path helpers. | +| `integration_test/` | pytest wrapper that runs the binary in the test container. | + +## Running + +Build and run the binary directly: + +```sh +bazel run //score/mw/com/test/inotify_stress_test:inotify_stress_test -- --num-processes 10 --cycles 100 +``` + +Run the integration test (10 processes, 300 cycles, distinct UIDs/GIDs from 2000): + +```sh +bazel test //score/mw/com/test/inotify_stress_test/integration_test:test_inotify_stress_test +``` diff --git a/score/mw/com/test/inotify_stress_test/controller.cpp b/score/mw/com/test/inotify_stress_test/controller.cpp new file mode 100644 index 000000000..6f5d9c3bf --- /dev/null +++ b/score/mw/com/test/inotify_stress_test/controller.cpp @@ -0,0 +1,91 @@ +/******************************************************************************* + * 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 "score/mw/com/test/inotify_stress_test/controller.h" + +#include "score/filesystem/details/standard_filesystem.h" +#include "score/mw/com/test/common_test_resources/check_point_control.h" +#include "score/mw/com/test/inotify_stress_test/inotify_stress_test_internal.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace score::mw::com::test +{ + +bool RunController(std::vector& checkpoint_controls, + const std::size_t num_processes, + const std::size_t cycles, + const score::cpp::stop_token& stop_token) +{ + const auto pid{getpid()}; + bool test_passed{true}; + + for (std::size_t i{0U}; i < cycles; ++i) + { + // Signal all workers to begin this cycle + for (std::size_t j{0U}; j < checkpoint_controls.size(); ++j) + { + std::cerr << pid << ": Signalling worker " << j << " to start (cycle " << i << ")" << std::endl; + checkpoint_controls[j]->ProceedToNextCheckpoint(); + } + + // Wait for every worker to report checkpoint or error — collect all before deciding to abort + for (std::size_t j{0U}; j < checkpoint_controls.size(); ++j) + { + const std::string tag{"[cycle=" + std::to_string(i) + " worker=" + std::to_string(j) + "] "}; + const int result = + WaitAndVerifyCheckPoint(tag, + *checkpoint_controls[j], + kCycleDoneCheckpoint, + stop_token, + std::chrono::duration_cast(kCheckpointWaitDuration)); + if (result != EXIT_SUCCESS) + { + std::cerr << pid << ": Worker " << j << " failed at cycle " << i << std::endl; + test_passed = false; + } + } + + // Remove the shared test directory so the next cycle exercises the creation path again + static_cast(num_processes); + const score::filesystem::StandardFilesystem fs; + const std::string test_dir{TestDir()}; + const auto remove = fs.RemoveAll(test_dir); + if (!remove.has_value()) + { + // Non-fatal: directory may not have been created (all workers errored before mkdir) + std::cerr << pid << ": Remove " << test_dir << " info: " << remove.error() << std::endl; + } + + if (!test_passed) + { + break; + } + } + + // Unblock all workers regardless of outcome so they exit cleanly + for (auto* const cp : checkpoint_controls) + { + cp->FinishActions(); + } + + return test_passed; +} + +} // namespace score::mw::com::test diff --git a/score/mw/com/test/inotify_stress_test/controller.h b/score/mw/com/test/inotify_stress_test/controller.h new file mode 100644 index 000000000..5c41eb57f --- /dev/null +++ b/score/mw/com/test/inotify_stress_test/controller.h @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + *******************************************************************************/ + +#ifndef SCORE_MW_COM_TEST_INOTIFY_STRESS_TEST_CONTROLLER_H +#define SCORE_MW_COM_TEST_INOTIFY_STRESS_TEST_CONTROLLER_H + +#include "score/mw/com/test/common_test_resources/check_point_control.h" + +#include + +#include +#include + +namespace score::mw::com::test +{ + +/// \brief Runs the controller loop for \p cycles iterations. +/// +/// Each iteration: +/// 1. Signals every worker via ProceedToNextCheckpoint(). +/// 2. Waits for every worker to report CheckPointReached or ErrorOccurred. +/// 3. Removes per-worker directories so the next cycle exercises the creation path. +/// +/// Sends FinishActions() to all workers before returning regardless of outcome. +/// +/// \return true if all cycles completed without any worker error; false otherwise. +bool RunController(std::vector& checkpoint_controls, + std::size_t num_processes, + std::size_t cycles, + const score::cpp::stop_token& stop_token); + +} // namespace score::mw::com::test + +#endif // SCORE_MW_COM_TEST_INOTIFY_STRESS_TEST_CONTROLLER_H diff --git a/score/mw/com/test/inotify_stress_test/inotify_stress_test.cpp b/score/mw/com/test/inotify_stress_test/inotify_stress_test.cpp new file mode 100644 index 000000000..da55617a2 --- /dev/null +++ b/score/mw/com/test/inotify_stress_test/inotify_stress_test.cpp @@ -0,0 +1,243 @@ +/******************************************************************************* + * 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 + *******************************************************************************/ + +/// \brief Inotify stress test — main entry point, argument parsing, setup and orchestration. +/// +/// Spawns M worker processes. Each worker is optionally assigned a unique UID/GID. Every worker +/// owns a dedicated sub-directory and file under the shared base folder. On each cycle the worker: +/// 1. Creates its directory (skipped when it already exists with the correct access rights). +/// 2. Creates its file (skipped when it already exists with the correct access rights). +/// 3. Adds an inotify watch on the shared base folder. +/// 4. Removes the watch immediately — no artificial delay. +/// +/// A controller (parent process) drives synchronisation via CheckPointControl: +/// - ProceedToNextCheckpoint() signals each worker to begin a cycle. +/// - WaitAndVerifyCheckPoint() waits for workers to report CheckPointReached or ErrorOccurred. +/// - After all cycles the controller sends FinishActions() so workers exit cleanly. + +#include "score/filesystem/details/standard_filesystem.h" +#include "score/mw/com/test/common_test_resources/child_process_guard.h" +#include "score/mw/com/test/common_test_resources/shared_memory_object_creator.h" +#include "score/mw/com/test/inotify_stress_test/controller.h" +#include "score/mw/com/test/inotify_stress_test/inotify_stress_test_internal.h" +#include "score/mw/com/test/inotify_stress_test/worker.h" + +#include "score/mw/com/test/common_test_resources/check_point_control.h" +#include "score/mw/com/test/common_test_resources/general_resources.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace score::mw::com::test +{ + +namespace +{ + +const std::string kShmNamePrefix{"inotify_stress_test_worker_"}; + +int DoSetup() +{ + score::filesystem::StandardFilesystem fs; + const auto create_dir = fs.CreateDirectory(kBaseFolder); + if (!create_dir.has_value()) + { + std::cerr << getpid() << ": Create base directory failed: " << create_dir.error() << std::endl; + return -1; + } + // Allow non-root worker processes (when --base-uid / --base-gid are used) to create + // subdirectories and files inside the shared base folder. + if (::chmod(kBaseFolder.c_str(), 0777) != 0) + { + std::cerr << getpid() << ": chmod on base directory failed: " << strerror(errno) << std::endl; + return -1; + } + std::cerr << getpid() << ": Created base directory: " << kBaseFolder << std::endl; + return 0; +} + +int ParseArguments(int argc, + const char** argv, + std::size_t& num_processes, + std::size_t& cycles, + std::uint32_t& base_uid, + std::uint32_t& base_gid) +{ + namespace po = boost::program_options; + + po::options_description options; + // clang-format off + options.add_options() + ("help", "Display the help message") + ("num-processes", po::value(&num_processes)->default_value(5U), + "Number of worker processes to spawn") + ("cycles", po::value(&cycles)->default_value(100U), + "Number of stress cycles before terminating") + ("base-uid", po::value(&base_uid)->default_value(0U), + "Base UID: worker N calls setuid(base-uid + N). 0 = skip setuid.") + ("base-gid", po::value(&base_gid)->default_value(0U), + "Base GID: worker N calls setgid(base-gid + N). 0 = skip setgid."); + // clang-format on + + po::variables_map args; + const auto parsed_args = + po::command_line_parser{argc, argv} + .options(options) + .style(po::command_line_style::unix_style | po::command_line_style::allow_long_disguise) + .run(); + po::store(parsed_args, args); + + if (args.count("help") > 0U) + { + std::cerr << options << std::endl; + return -1; + } + + po::notify(args); + return 0; +} + +/// \brief Creates SHM checkpoints, forks workers, runs the controller loop, then cleans up. +int RunStressTest(const std::size_t num_processes, + const std::size_t cycles, + const std::uint32_t base_uid, + const std::uint32_t base_gid) +{ + const score::cpp::stop_source stop_source{}; + + // Create one CheckPointControl in shared memory per worker — must happen BEFORE fork + std::vector> shmem_guards{}; + shmem_guards.reserve(num_processes); + + // CheckPointControl stores the owner name as a non-owning std::string_view, so the backing + // strings must outlive every CheckPointControl — keep them alive for the whole function. + std::vector owner_names{}; + owner_names.reserve(num_processes); + + for (std::size_t i{0U}; i < num_processes; ++i) + { + const std::string shm_name{kShmNamePrefix + std::to_string(i)}; + const std::string& owner_name{owner_names.emplace_back("Worker_" + std::to_string(i))}; + auto guard_result = CreateOrOpenSharedCheckPointControl("main", shm_name, owner_name); + if (!guard_result.has_value()) + { + std::cerr << "Failed to create CheckPointControl for worker " << i << std::endl; + return -1; + } + shmem_guards.push_back(std::move(guard_result.value())); + } + + // Collect raw pointers — SHM addresses remain stable across the vector lifetime + std::vector checkpoint_controls{}; + checkpoint_controls.reserve(num_processes); + for (auto& guard : shmem_guards) + { + checkpoint_controls.push_back(&guard.GetObject()); + } + + // Fork worker processes + std::vector child_guards{}; + child_guards.reserve(num_processes); + + for (std::size_t worker{0U}; worker < num_processes; ++worker) + { + CheckPointControl& cp = *checkpoint_controls[worker]; + const std::string worker_name{"Worker_" + std::to_string(worker)}; + + auto guard_opt = ForkProcessAndRunInChildProcess( + "main", worker_name, [&cp, worker, cycles, base_uid, base_gid, worker_name]() { + if (!SetWorkerCredentials(worker_name, base_gid, base_uid, worker)) + { + cp.ErrorOccurred(); + return; + } + RunWorkerProcess(worker, cycles, cp); + }); + + if (!guard_opt.has_value()) + { + std::cerr << "Failed to fork worker " << worker << ", aborting." << std::endl; + for (auto& g : child_guards) + { + static_cast(g.KillChildProcess()); + } + return -1; + } + child_guards.push_back(guard_opt.value()); + } + + const bool test_passed = RunController(checkpoint_controls, num_processes, cycles, stop_source.get_token()); + + // Wait for all worker processes to exit + constexpr std::chrono::milliseconds worker_termination_timeout{5000U}; + for (auto& guard : child_guards) + { + if (!WaitForChildProcessToTerminate("main", guard, worker_termination_timeout)) + { + std::cerr << "Worker PID " << guard.GetPid() << " did not terminate in time, killing." << std::endl; + static_cast(guard.KillChildProcess()); + } + } + + // Release shared memory + for (auto& guard : shmem_guards) + { + guard.CleanUp(); + } + + return test_passed ? EXIT_SUCCESS : EXIT_FAILURE; +} + +} // namespace + +} // namespace score::mw::com::test + +int main(int argc, const char** argv) +{ + std::cerr << "inotify_stress_test: starting" << std::endl; + + std::size_t num_processes{0U}; + std::size_t cycles{0U}; + std::uint32_t base_uid{0U}; + std::uint32_t base_gid{0U}; + + if (score::mw::com::test::ParseArguments(argc, argv, num_processes, cycles, base_uid, base_gid) == -1) + { + return -1; + } + + if (score::mw::com::test::DoSetup() == -1) + { + return -1; + } + + return score::mw::com::test::RunStressTest(num_processes, cycles, base_uid, base_gid); +} diff --git a/score/mw/com/test/inotify_stress_test/inotify_stress_test_internal.h b/score/mw/com/test/inotify_stress_test/inotify_stress_test_internal.h new file mode 100644 index 000000000..097265cc0 --- /dev/null +++ b/score/mw/com/test/inotify_stress_test/inotify_stress_test_internal.h @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + *******************************************************************************/ + +#ifndef SCORE_MW_COM_TEST_INOTIFY_STRESS_TEST_INTERNAL_H +#define SCORE_MW_COM_TEST_INOTIFY_STRESS_TEST_INTERNAL_H + +#include +#include +#include + +#if defined(__QNXNTO__) +inline const std::string kBaseFolder{"/tmp_discovery/inotify_stress_test"}; +#else +inline const std::string kBaseFolder{"/tmp/inotify_stress_test"}; +#endif + +namespace score::mw::com::test +{ + +/// Checkpoint number reported by each worker upon completing one stress cycle. +constexpr std::uint8_t kCycleDoneCheckpoint{1U}; + +/// Maximum time the controller waits for a single worker to complete a cycle. +constexpr std::chrono::seconds kCheckpointWaitDuration{30U}; + +/// Returns the single shared directory that every worker concurrently attempts to create. +inline std::string TestDir() +{ + return kBaseFolder + "/shared_process"; +} + +} // namespace score::mw::com::test + +#endif // SCORE_MW_COM_TEST_INOTIFY_STRESS_TEST_INTERNAL_H diff --git a/score/mw/com/test/inotify_stress_test/integration_test/BUILD b/score/mw/com/test/inotify_stress_test/integration_test/BUILD new file mode 100644 index 000000000..ea6e2de24 --- /dev/null +++ b/score/mw/com/test/inotify_stress_test/integration_test/BUILD @@ -0,0 +1,20 @@ +# ******************************************************************************* +# 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("//quality/integration_testing:integration_testing.bzl", "integration_test") + +integration_test( + name = "inotify_stress_test", + srcs = ["test_inotify_stress_test.py"], + filesystem = "//score/mw/com/test/inotify_stress_test:inotify-stress-test-pkg", +) diff --git a/score/mw/com/test/inotify_stress_test/integration_test/test_inotify_stress_test.py b/score/mw/com/test/inotify_stress_test/integration_test/test_inotify_stress_test.py new file mode 100644 index 000000000..d3090718a --- /dev/null +++ b/score/mw/com/test/inotify_stress_test/integration_test/test_inotify_stress_test.py @@ -0,0 +1,37 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +"""Integration test for the inotify stress test.""" + + +def inotify_stress_test(target, **kwargs): + args = [ + "--num-processes", "10", + "--cycles", "300", + "--base-uid", "2000", + "--base-gid", "2000", + ] + return target.wrap_exec( + "bin/inotify_stress_test", + args, + cwd="/opt/InotifyStressTestApp", + wait_on_exit=True, + **kwargs, + ) + + +def test_inotify_stress(target): + """Stress test for inotify: M processes concurrently create directories, files, + set access rights, and add/remove watches over N cycles.""" + with inotify_stress_test(target, wait_timeout=50): + pass diff --git a/score/mw/com/test/inotify_stress_test/worker.cpp b/score/mw/com/test/inotify_stress_test/worker.cpp new file mode 100644 index 000000000..a575be40e --- /dev/null +++ b/score/mw/com/test/inotify_stress_test/worker.cpp @@ -0,0 +1,204 @@ +/******************************************************************************* + * 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 "score/mw/com/test/inotify_stress_test/worker.h" + +#include "score/mw/com/test/common_test_resources/check_point_control.h" +#include "score/mw/com/test/common_test_resources/general_resources.h" +#include "score/mw/com/test/inotify_stress_test/inotify_stress_test_internal.h" +#include "score/os/inotify.h" +#include "score/os/utils/inotify/inotify_instance_impl.h" + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +namespace score::mw::com::test +{ + +namespace +{ + +constexpr mode_t kExpectedDirMode{0755U}; + +/// \brief Maximum number of add+remove attempts per cycle before a worker reports failure. Tolerates the +/// transient EINVAL the QNX io-notify manager can return under heavy concurrent watch churn. +constexpr std::size_t kMaxWatchAttempts{5U}; + +/// \brief Returns true when \p path exists and its lower nine permission bits match \p expected_mode. +bool HasCorrectPermissions(const pid_t pid, const std::string& path, const mode_t expected_mode) +{ + struct stat st{}; + if (::stat(path.c_str(), &st) != 0) + { + std::cerr << pid << ": stat on " << path << " failed: " << strerror(errno) << std::endl; + return false; + } + if ((st.st_mode & 0777U) != expected_mode) + { + std::cerr << pid << ": Directory " << path << " has wrong permissions: actual " << std::oct + << (st.st_mode & 0777U) << ", expected " << expected_mode << std::dec << std::endl; + return false; + } + return true; +} + +/// \brief Ensures the shared directory exists with the expected mode. Creates it when absent, otherwise +/// verifies its permissions. Returns false (after logging) on any failure. +bool EnsureDirectory(const pid_t pid, const std::string& test_dir) +{ + const int result{::mkdir(test_dir.c_str(), kExpectedDirMode)}; + if (result == 0) + { + // This process created the directory and owns it — safe to chmod. + if (::chmod(test_dir.c_str(), kExpectedDirMode) != 0) + { + std::cerr << pid << ": chmod on " << test_dir << " failed: " << strerror(errno) << std::endl; + return false; + } + std::cerr << pid << ": Created directory: " << test_dir << std::endl; + return true; + } + if (errno == EEXIST) + { + // Another worker already created it — only verify permissions, never chmod. + // HasCorrectPermissions logs the specific reason (stat failure or mode mismatch) on failure. + if (!HasCorrectPermissions(pid, test_dir, kExpectedDirMode)) + { + return false; + } + std::cerr << pid << ": Directory already exists, skipping: " << test_dir << std::endl; + return true; + } + std::cerr << pid << ": mkdir " << test_dir << " failed: " << strerror(errno) << std::endl; + return false; +} + +} // namespace + +void RunWorkerProcess(const std::size_t worker_index, const std::size_t cycles, CheckPointControl& checkpoint_control) +{ + const auto pid{getpid()}; + const std::string test_dir{TestDir()}; + static_cast(worker_index); + + const score::cpp::stop_source worker_stop_source{}; + os::InotifyInstanceImpl inotify{}; + + for (std::size_t i{0U}; i < cycles; ++i) + { + // Wait for controller's start-of-cycle signal — blocks without polling + const auto instruction = WaitForChildProceed(checkpoint_control, worker_stop_source.get_token()); + if (instruction != CheckPointControl::ProceedInstruction::PROCEED_NEXT_CHECKPOINT) + { + std::cerr << pid << ": Received non-proceed instruction at cycle " << i << ", exiting." << std::endl; + return; + } + + // Step 1: Ensure the shared process directory exists with the expected permissions + if (!EnsureDirectory(pid, test_dir)) + { + checkpoint_control.ErrorOccurred(); + return; + } + + // Steps 2 & 3: Add an inotify watch on the shared base folder and remove it immediately. + // Under heavy concurrent add/remove churn on the same directory, the QNX io-notify resource + // manager can transiently fail a removal with EINVAL. Once that happens the watch descriptor is + // permanently invalid — retrying RemoveWatch on the same descriptor keeps returning EINVAL — so + // recovery requires obtaining a fresh descriptor via AddWatch. Therefore the whole add+remove + // step is retried a bounded number of times, and the worker only fails if it cannot complete a + // clean add+remove within kMaxWatchAttempts. Any non-EINVAL error is a genuine failure. + bool watch_cycle_succeeded{false}; + for (std::size_t attempt{0U}; (attempt < kMaxWatchAttempts) && (!watch_cycle_succeeded); ++attempt) + { + auto watch_descriptor = + inotify.AddWatch(kBaseFolder, os::Inotify::EventMask::kInCreate | os::Inotify::EventMask::kInDelete); + if (!watch_descriptor.has_value()) + { + std::cerr << pid << ": AddWatch failed: " << watch_descriptor.error() << " on " << kBaseFolder + << std::endl; + checkpoint_control.ErrorOccurred(); + return; + } + + const auto remove_result = inotify.RemoveWatch(watch_descriptor.value()); + if (remove_result.has_value()) + { + watch_cycle_succeeded = true; + } + else if (remove_result.error().GetOsDependentErrorCode() != EINVAL) + { + std::cerr << pid << ": RemoveWatch failed: " << remove_result.error() << std::endl; + checkpoint_control.ErrorOccurred(); + return; + } + else + { + // Transient EINVAL: the descriptor is now invalid, so the next attempt re-adds the watch + // to obtain a fresh descriptor before removing again. + std::cerr << pid << ": RemoveWatch transient EINVAL (attempt " << (attempt + 1U) << " of " + << kMaxWatchAttempts << "), re-adding watch and retrying" << std::endl; + } + } + + if (!watch_cycle_succeeded) + { + std::cerr << pid << ": RemoveWatch failed persistently after " << kMaxWatchAttempts << " attempts" + << std::endl; + checkpoint_control.ErrorOccurred(); + return; + } + + // Notify controller that this cycle completed successfully + checkpoint_control.CheckPointReached(kCycleDoneCheckpoint); + } + + // Wait for controller's final FinishActions signal before exiting + static_cast(WaitForChildProceed(checkpoint_control, worker_stop_source.get_token())); + inotify.Close(); +} + +bool SetWorkerCredentials(const std::string& worker_name, + const std::uint32_t base_gid, + const std::uint32_t base_uid, + const std::size_t worker_index) +{ + if (base_gid != 0U) + { + if (::setgid(static_cast(base_gid + static_cast(worker_index))) != 0) + { + std::cerr << worker_name << ": setgid failed: " << strerror(errno) << std::endl; + return false; + } + } + if (base_uid != 0U) + { + if (::setuid(static_cast(base_uid + static_cast(worker_index))) != 0) + { + std::cerr << worker_name << ": setuid failed: " << strerror(errno) << std::endl; + return false; + } + } + return true; +} + +} // namespace score::mw::com::test diff --git a/score/mw/com/test/inotify_stress_test/worker.h b/score/mw/com/test/inotify_stress_test/worker.h new file mode 100644 index 000000000..6020658fa --- /dev/null +++ b/score/mw/com/test/inotify_stress_test/worker.h @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + *******************************************************************************/ + +#ifndef SCORE_MW_COM_TEST_INOTIFY_STRESS_TEST_WORKER_H +#define SCORE_MW_COM_TEST_INOTIFY_STRESS_TEST_WORKER_H + +#include "score/mw/com/test/common_test_resources/check_point_control.h" + +#include +#include +#include + +namespace score::mw::com::test +{ + +/// \brief Entry point executed in each forked worker process. +/// +/// Runs \p cycles iterations. Each iteration: +/// 1. Waits for the controller's start signal. +/// 2. Creates (or verifies) its dedicated directory and file under kBaseFolder. +/// 3. Adds an inotify watch on kBaseFolder, then removes it immediately. +/// 4. Reports CheckPointReached to the controller. +void RunWorkerProcess(std::size_t worker_index, std::size_t cycles, CheckPointControl& checkpoint_control); + +/// \brief Sets the GID and UID of the calling process for worker \p worker_index. +/// +/// GID is changed before UID so that root privileges are still held when setgid is called. +/// If \p base_gid or \p base_uid is zero the corresponding call is skipped. +/// +/// \return true if all requested credential changes succeeded; false otherwise. +bool SetWorkerCredentials(const std::string& worker_name, + std::uint32_t base_gid, + std::uint32_t base_uid, + std::size_t worker_index); + +} // namespace score::mw::com::test + +#endif // SCORE_MW_COM_TEST_INOTIFY_STRESS_TEST_WORKER_H