diff options
| author | Stefan Boberg <[email protected]> | 2026-03-24 15:47:23 +0100 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-03-24 15:47:23 +0100 |
| commit | 21c2abb1bde697c31bee562465cb986a0429a299 (patch) | |
| tree | 3734d235e79a8fbed307ae5c248936d356553b61 /src/zenutil/include | |
| parent | v5.7.25 hotpatch (#874) (diff) | |
| download | zen-21c2abb1bde697c31bee562465cb986a0429a299.tar.xz zen-21c2abb1bde697c31bee562465cb986a0429a299.zip | |
Subprocess Manager (#889)
Adds a `SubprocessManager` for managing child processes with ASIO-integrated async exit detection, stdout/stderr pipe capture, and periodic metrics sampling. Also introduces `ProcessGroup` for OS-backed process grouping (Windows JobObjects / POSIX process groups).
### SubprocessManager
- Async process exit detection using platform-native mechanisms (Windows `object_handle`, Linux `pidfd_open`, macOS `kqueue EVFILT_PROC`) — no polling
- Stdout/stderr capture via async pipe readers with per-process or default callbacks
- Periodic round-robin metrics sampling (CPU, memory) across managed processes
- Spawn, adopt, remove, kill, and enumerate managed processes
### ProcessGroup
- OS-level process grouping: Windows JobObject (kill-on-close guarantee), POSIX `setpgid` (bulk signal delivery)
- Atomic group kill via `TerminateJobObject` (Windows) or `kill(-pgid, sig)` (POSIX)
- Per-group aggregate metrics and enumeration
### ProcessHandle improvements
- Added explicit constructors from `int` (pid) and `void*` (native handle)
- Added move constructor and move assignment operator
### ProcessMetricsTracker
- Cross-platform process metrics (CPU time, working set, page faults) via `QueryProcessMetrics()`
- ASIO timer-driven periodic sampling with configurable interval and batch size
- Aggregate metrics across tracked processes
### Other changes
- Fixed `zentest-appstub` writing a spurious `Versions` file to cwd on every invocation
Diffstat (limited to 'src/zenutil/include')
| -rw-r--r-- | src/zenutil/include/zenutil/process/subprocessmanager.h | 283 | ||||
| -rw-r--r-- | src/zenutil/include/zenutil/processmetricstracker.h | 105 |
2 files changed, 283 insertions, 105 deletions
diff --git a/src/zenutil/include/zenutil/process/subprocessmanager.h b/src/zenutil/include/zenutil/process/subprocessmanager.h new file mode 100644 index 000000000..4a25170df --- /dev/null +++ b/src/zenutil/include/zenutil/process/subprocessmanager.h @@ -0,0 +1,283 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include <zencore/process.h> +#include <zencore/zencore.h> + +#include <filesystem> +#include <functional> +#include <memory> +#include <string> +#include <string_view> +#include <vector> + +namespace zen { + +/** Tracked process entry with latest metrics snapshot. + */ +struct TrackedProcessEntry +{ + int Pid = 0; + ProcessMetrics Metrics; + + // Derived CPU usage percentage (delta-based, requires two samples). + // -1.0 means not yet sampled. + float CpuUsagePercent = -1.0f; +}; + +/** Aggregate metrics across all tracked processes. + */ +struct AggregateProcessMetrics +{ + uint64_t TotalWorkingSetSize = 0; + uint64_t TotalPeakWorkingSetSize = 0; + uint64_t TotalUserTimeMs = 0; + uint64_t TotalKernelTimeMs = 0; + uint32_t ProcessCount = 0; +}; + +} // namespace zen + +namespace asio { +class io_context; +} + +namespace zen { + +class ManagedProcess; +class ProcessGroup; + +/// Callback invoked when a managed process exits. +using ProcessExitCallback = std::function<void(ManagedProcess& Process, int ExitCode)>; + +/// Callback invoked when data is read from a managed process's stdout or stderr. +using ProcessDataCallback = std::function<void(ManagedProcess& Process, std::string_view Data)>; + +/// Configuration for SubprocessManager. +struct SubprocessManagerConfig +{ + /// Interval for periodic metrics sampling. Set to 0 to disable. + uint64_t MetricsSampleIntervalMs = 5000; + + /// Number of processes sampled per metrics tick (round-robin). + uint32_t MetricsBatchSize = 16; +}; + +/// Manages a set of child processes with async exit detection, stdout/stderr +/// capture, and periodic metrics sampling. +/// +/// All callbacks are posted to the io_context and never invoked under internal +/// locks. The caller must ensure the io_context outlives this manager and that +/// its run loop is active. +/// +/// Usage: +/// asio::io_context IoContext; +/// SubprocessManager Manager(IoContext); +/// +/// StdoutPipeHandles StdoutPipe; +/// CreateStdoutPipe(StdoutPipe); +/// +/// CreateProcOptions Options; +/// Options.StdoutPipe = &StdoutPipe; +/// +/// auto* Proc = Manager.Spawn(Executable, CommandLine, Options, +/// [](ManagedProcess& P, int Code) { ... }); +class SubprocessManager +{ +public: + explicit SubprocessManager(asio::io_context& IoContext, SubprocessManagerConfig Config = {}); + ~SubprocessManager(); + + SubprocessManager(const SubprocessManager&) = delete; + SubprocessManager& operator=(const SubprocessManager&) = delete; + + /// Spawn a new child process and begin monitoring it. + /// + /// If Options.StdoutPipe is set, the pipe is consumed and async reading + /// begins automatically. Similarly for Options.StderrPipe. + /// + /// Returns a non-owning pointer valid until Remove() or manager destruction. + /// The exit callback fires on an io_context thread when the process terminates. + ManagedProcess* Spawn(const std::filesystem::path& Executable, + std::string_view CommandLine, + CreateProcOptions& Options, + ProcessExitCallback OnExit); + + /// Adopt an already-running process by handle. Takes ownership of handle internals. + ManagedProcess* Adopt(ProcessHandle&& Handle, ProcessExitCallback OnExit); + + /// Stop monitoring a process by pid. Does NOT kill the process — call + /// process->Kill() first if needed. The exit callback will not fire after + /// this returns. + void Remove(int Pid); + + /// Remove all managed processes. + void RemoveAll(); + + /// Set default stdout callback. Per-process callbacks override this. + void SetDefaultStdoutCallback(ProcessDataCallback Callback); + + /// Set default stderr callback. Per-process callbacks override this. + void SetDefaultStderrCallback(ProcessDataCallback Callback); + + /// Snapshot of per-process metrics for all managed processes. + [[nodiscard]] std::vector<TrackedProcessEntry> GetMetricsSnapshot() const; + + /// Aggregate metrics across all managed processes. + [[nodiscard]] AggregateProcessMetrics GetAggregateMetrics() const; + + /// Number of currently managed processes. + [[nodiscard]] size_t GetProcessCount() const; + + /// Enumerate all managed processes under a shared lock. + void Enumerate(std::function<void(const ManagedProcess&)> Callback) const; + + /// Create a new process group. The group is owned by this manager. + /// On Windows the group is backed by a JobObject (kill-on-close guarantee). + /// On POSIX the group uses setpgid for bulk signal delivery. + ProcessGroup* CreateGroup(std::string Name); + + /// Destroy a group by name. Kills all processes in the group first. + void DestroyGroup(std::string_view Name); + + /// Find a group by name. Returns nullptr if not found. + [[nodiscard]] ProcessGroup* FindGroup(std::string_view Name) const; + + /// Enumerate all groups. + void EnumerateGroups(std::function<void(const ProcessGroup&)> Callback) const; + +private: + friend class ProcessGroup; + + struct Impl; + std::unique_ptr<Impl> m_Impl; +}; + +/// A process managed by SubprocessManager. +/// +/// Not user-constructible. Pointers obtained from Spawn()/Adopt() remain valid +/// until Remove() or manager destruction. +class ManagedProcess +{ +public: + ~ManagedProcess(); + + ManagedProcess(const ManagedProcess&) = delete; + ManagedProcess& operator=(const ManagedProcess&) = delete; + + /// Process id. + [[nodiscard]] int Pid() const; + + /// Whether the process is still running. + [[nodiscard]] bool IsRunning() const; + + /// Underlying process handle. + [[nodiscard]] const ProcessHandle& GetHandle() const; + + /// Most recently sampled metrics (best-effort snapshot). + [[nodiscard]] ProcessMetrics GetLatestMetrics() const; + + /// CPU usage percentage from the last two samples. Returns -1.0 if not + /// yet computed. + [[nodiscard]] float GetCpuUsagePercent() const; + + /// Set per-process stdout callback (overrides manager default). + void SetStdoutCallback(ProcessDataCallback Callback); + + /// Set per-process stderr callback (overrides manager default). + void SetStderrCallback(ProcessDataCallback Callback); + + /// Return all stdout captured so far. When a callback is set, output is + /// delivered there instead of being accumulated. + [[nodiscard]] std::string GetCapturedStdout() const; + + /// Return all stderr captured so far. + [[nodiscard]] std::string GetCapturedStderr() const; + + /// Graceful shutdown with fallback to forced kill. + bool Kill(); + + /// Immediate forced termination. + bool Terminate(int ExitCode); + + /// User-defined tag for identifying this process in callbacks. + void SetTag(std::string Tag); + + /// Get the user-defined tag. + [[nodiscard]] std::string_view GetTag() const; + +private: + friend class SubprocessManager; + friend class ProcessGroup; + + struct Impl; + std::unique_ptr<Impl> m_Impl; + + explicit ManagedProcess(std::unique_ptr<Impl> InImpl); +}; + +/// A group of managed processes with OS-level backing. +/// +/// On Windows: backed by a JobObject. All processes assigned on spawn. +/// Kill-on-close guarantee — if the group is destroyed, the OS terminates +/// all member processes. +/// On Linux/macOS: uses setpgid() so children share a process group. +/// Enables bulk signal delivery via kill(-pgid, sig). +/// +/// Created via SubprocessManager::CreateGroup(). Not user-constructible. +class ProcessGroup +{ +public: + ~ProcessGroup(); + + ProcessGroup(const ProcessGroup&) = delete; + ProcessGroup& operator=(const ProcessGroup&) = delete; + + /// Group name (as passed to CreateGroup). + [[nodiscard]] std::string_view GetName() const; + + /// Spawn a process into this group. + ManagedProcess* Spawn(const std::filesystem::path& Executable, + std::string_view CommandLine, + CreateProcOptions& Options, + ProcessExitCallback OnExit); + + /// Adopt an already-running process into this group. + /// On Windows the process is assigned to the group's JobObject. + /// On POSIX the process cannot be moved into a different process group + /// after creation, so OS-level grouping is best-effort for adopted processes. + ManagedProcess* Adopt(ProcessHandle&& Handle, ProcessExitCallback OnExit); + + /// Remove a process from this group. Does NOT kill it. + void Remove(int Pid); + + /// Kill all processes in the group. + /// On Windows: uses TerminateJobObject for atomic group kill. + /// On POSIX: sends SIGTERM then SIGKILL to the process group. + void KillAll(); + + /// Aggregate metrics for this group's processes. + [[nodiscard]] AggregateProcessMetrics GetAggregateMetrics() const; + + /// Per-process metrics snapshot for this group. + [[nodiscard]] std::vector<TrackedProcessEntry> GetMetricsSnapshot() const; + + /// Number of processes in this group. + [[nodiscard]] size_t GetProcessCount() const; + + /// Enumerate processes in this group. + void Enumerate(std::function<void(const ManagedProcess&)> Callback) const; + +private: + friend class SubprocessManager; + + struct Impl; + std::unique_ptr<Impl> m_Impl; + + explicit ProcessGroup(std::unique_ptr<Impl> InImpl); +}; + +void subprocessmanager_forcelink(); // internal + +} // namespace zen diff --git a/src/zenutil/include/zenutil/processmetricstracker.h b/src/zenutil/include/zenutil/processmetricstracker.h deleted file mode 100644 index fdeae2bfa..000000000 --- a/src/zenutil/include/zenutil/processmetricstracker.h +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#pragma once - -#include <zencore/process.h> -#include <zencore/zencore.h> - -#include <memory> -#include <vector> - -namespace asio { -class io_context; -} - -namespace zen { - -/** Tracked process entry with latest metrics snapshot. - */ -struct TrackedProcessEntry -{ - int Pid = 0; - ProcessMetrics Metrics; - - // Derived CPU usage percentage (delta-based, requires two samples). - // -1.0 means not yet sampled. - float CpuUsagePercent = -1.0f; -}; - -/** Aggregate metrics across all tracked processes. - */ -struct AggregateProcessMetrics -{ - uint64_t TotalWorkingSetSize = 0; - uint64_t TotalPeakWorkingSetSize = 0; - uint64_t TotalUserTimeMs = 0; - uint64_t TotalKernelTimeMs = 0; - uint32_t ProcessCount = 0; -}; - -/** Background process metrics tracker. - * - * Maintains a set of child processes keyed by pid and periodically samples - * their resource usage (CPU times, memory) in a background thread or via - * an ASIO timer on an external io_context. - * - * The tracker does not take ownership of process handles. On Windows it - * duplicates the handle internally; on other platforms it uses the pid - * directly. - * - * Usage (dedicated thread): - * ProcessMetricsTracker Tracker; - * Tracker.Start(); - * Tracker.Add(ChildHandle); - * - * Usage (ASIO timer): - * ProcessMetricsTracker Tracker(IoContext); - * Tracker.Start(); - * Tracker.Add(ChildHandle); - */ -class ProcessMetricsTracker -{ -public: - /// Construct with a dedicated background thread for sampling. - explicit ProcessMetricsTracker(uint64_t SampleIntervalMs = 5000); - - /// Construct with an external io_context — uses an asio::steady_timer - /// instead of a dedicated thread. The caller must ensure the io_context - /// outlives this tracker and that its run loop is active. - ProcessMetricsTracker(asio::io_context& IoContext, uint64_t SampleIntervalMs = 5000); - - ~ProcessMetricsTracker(); - - ProcessMetricsTracker(const ProcessMetricsTracker&) = delete; - ProcessMetricsTracker& operator=(const ProcessMetricsTracker&) = delete; - - /// Start sampling. Spawns the background thread or enqueues the first timer. - void Start(); - - /// Stop sampling. Safe to call multiple times. - void Stop(); - - /// Add a process to track. Internally clones the handle (Windows) or - /// copies the pid (Linux/macOS). If the pid is already tracked, replaces it. - void Add(const ProcessHandle& Handle); - - /// Remove a tracked process by pid. - void Remove(int Pid); - - /// Remove all tracked processes. - void Clear(); - - /// Returns a snapshot of metrics for all tracked processes. - std::vector<TrackedProcessEntry> GetSnapshot() const; - - /// Returns aggregate metrics across all tracked processes. - AggregateProcessMetrics GetAggregate() const; - -private: - struct Impl; - std::unique_ptr<Impl> m_Impl; -}; - -void processmetricstracker_forcelink(); // internal - -} // namespace zen |