aboutsummaryrefslogtreecommitdiff
path: root/src/zenutil/include
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-03-24 15:47:23 +0100
committerGitHub Enterprise <[email protected]>2026-03-24 15:47:23 +0100
commit21c2abb1bde697c31bee562465cb986a0429a299 (patch)
tree3734d235e79a8fbed307ae5c248936d356553b61 /src/zenutil/include
parentv5.7.25 hotpatch (#874) (diff)
downloadzen-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.h283
-rw-r--r--src/zenutil/include/zenutil/processmetricstracker.h105
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