From 21c2abb1bde697c31bee562465cb986a0429a299 Mon Sep 17 00:00:00 2001 From: Stefan Boberg Date: Tue, 24 Mar 2026 15:47:23 +0100 Subject: Subprocess Manager (#889) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/zenutil/process/exitwatcher.cpp | 294 ++++++++++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 src/zenutil/process/exitwatcher.cpp (limited to 'src/zenutil/process/exitwatcher.cpp') diff --git a/src/zenutil/process/exitwatcher.cpp b/src/zenutil/process/exitwatcher.cpp new file mode 100644 index 000000000..cef31ebca --- /dev/null +++ b/src/zenutil/process/exitwatcher.cpp @@ -0,0 +1,294 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "exitwatcher.h" + +#include + +ZEN_THIRD_PARTY_INCLUDES_START + +#if ZEN_PLATFORM_WINDOWS +# include +# include +# include +#elif ZEN_PLATFORM_LINUX +# include +# include +# include +# include +# include + +# ifndef SYS_pidfd_open +# define SYS_pidfd_open 434 // x86_64 +# endif +#elif ZEN_PLATFORM_MAC +# include +# include +# include +# include +# include +#endif + +ZEN_THIRD_PARTY_INCLUDES_END + +namespace zen { + +// ============================================================================ +// Linux: pidfd_open + stream_descriptor +// ============================================================================ + +#if ZEN_PLATFORM_LINUX + +struct ProcessExitWatcher::Impl +{ + asio::io_context& m_IoContext; + std::unique_ptr m_Descriptor; + int m_PidFd = -1; + int m_Pid = 0; + + explicit Impl(asio::io_context& IoContext) : m_IoContext(IoContext) {} + + ~Impl() { Cancel(); } + + void Watch(const ProcessHandle& Handle, std::function OnExit) + { + m_Pid = Handle.Pid(); + + // pidfd_open returns an fd that becomes readable when the process exits. + // Available since Linux 5.3. + m_PidFd = static_cast(syscall(SYS_pidfd_open, m_Pid, 0)); + if (m_PidFd < 0) + { + ZEN_WARN("pidfd_open failed for pid {}: {}", m_Pid, strerror(errno)); + return; + } + + m_Descriptor = std::make_unique(m_IoContext, m_PidFd); + + m_Descriptor->async_wait(asio::posix::stream_descriptor::wait_read, + [this, Callback = std::move(OnExit)](const asio::error_code& Ec) { + if (Ec) + { + return; // Cancelled or error + } + + int ExitCode = -1; + int Status = 0; + // The pidfd told us the process exited. Reap it with waitpid. + if (waitpid(m_Pid, &Status, WNOHANG) > 0) + { + if (WIFEXITED(Status)) + { + ExitCode = WEXITSTATUS(Status); + } + else if (WIFSIGNALED(Status)) + { + constexpr int kSignalExitBase = 128; + ExitCode = kSignalExitBase + WTERMSIG(Status); + } + } + + Callback(ExitCode); + }); + } + + void Cancel() + { + if (m_Descriptor) + { + asio::error_code Ec; + m_Descriptor->cancel(Ec); + m_Descriptor.reset(); + // stream_descriptor closes the fd on destruction, so don't close m_PidFd separately + m_PidFd = -1; + } + else if (m_PidFd >= 0) + { + close(m_PidFd); + m_PidFd = -1; + } + } +}; + +// ============================================================================ +// Windows: object_handle::async_wait +// ============================================================================ + +#elif ZEN_PLATFORM_WINDOWS + +struct ProcessExitWatcher::Impl +{ + asio::io_context& m_IoContext; + std::unique_ptr m_ObjectHandle; + void* m_DuplicatedHandle = nullptr; + + explicit Impl(asio::io_context& IoContext) : m_IoContext(IoContext) {} + + ~Impl() { Cancel(); } + + void Watch(const ProcessHandle& Handle, std::function OnExit) + { + // Duplicate the process handle so ASIO can take ownership independently + HANDLE SourceHandle = static_cast(Handle.Handle()); + HANDLE CurrentProcess = GetCurrentProcess(); + BOOL Success = DuplicateHandle(CurrentProcess, + SourceHandle, + CurrentProcess, + reinterpret_cast(&m_DuplicatedHandle), + SYNCHRONIZE | PROCESS_QUERY_INFORMATION, + FALSE, + 0); + + if (!Success) + { + ZEN_WARN("DuplicateHandle failed for pid {}: {}", Handle.Pid(), GetLastError()); + return; + } + + // object_handle takes ownership of the handle + m_ObjectHandle = std::make_unique(m_IoContext, m_DuplicatedHandle); + + m_ObjectHandle->async_wait([this, DupHandle = m_DuplicatedHandle, Callback = std::move(OnExit)](const asio::error_code& Ec) { + if (Ec) + { + return; + } + + DWORD ExitCode = 0; + GetExitCodeProcess(static_cast(DupHandle), &ExitCode); + Callback(static_cast(ExitCode)); + }); + } + + void Cancel() + { + if (m_ObjectHandle) + { + asio::error_code Ec; + m_ObjectHandle->cancel(Ec); + m_ObjectHandle.reset(); // Closes the duplicated handle + m_DuplicatedHandle = nullptr; + } + else if (m_DuplicatedHandle) + { + CloseHandle(static_cast(m_DuplicatedHandle)); + m_DuplicatedHandle = nullptr; + } + } +}; + +// ============================================================================ +// macOS: kqueue EVFILT_PROC + stream_descriptor +// ============================================================================ + +#elif ZEN_PLATFORM_MAC + +struct ProcessExitWatcher::Impl +{ + asio::io_context& m_IoContext; + std::unique_ptr m_Descriptor; + int m_KqueueFd = -1; + int m_Pid = 0; + + explicit Impl(asio::io_context& IoContext) : m_IoContext(IoContext) {} + + ~Impl() { Cancel(); } + + void Watch(const ProcessHandle& Handle, std::function OnExit) + { + m_Pid = Handle.Pid(); + + m_KqueueFd = kqueue(); + if (m_KqueueFd < 0) + { + ZEN_WARN("kqueue() failed for pid {}: {}", m_Pid, strerror(errno)); + return; + } + + // Register interest in the process exit event + struct kevent Change; + EV_SET(&Change, static_cast(m_Pid), EVFILT_PROC, EV_ADD | EV_ONESHOT, NOTE_EXIT, 0, nullptr); + + if (kevent(m_KqueueFd, &Change, 1, nullptr, 0, nullptr) < 0) + { + ZEN_WARN("kevent register failed for pid {}: {}", m_Pid, strerror(errno)); + close(m_KqueueFd); + m_KqueueFd = -1; + return; + } + + m_Descriptor = std::make_unique(m_IoContext, m_KqueueFd); + + m_Descriptor->async_wait(asio::posix::stream_descriptor::wait_read, + [this, Callback = std::move(OnExit)](const asio::error_code& Ec) { + if (Ec) + { + return; + } + + // Drain the kqueue event + struct kevent Event; + struct timespec Timeout = {0, 0}; + kevent(m_KqueueFd, nullptr, 0, &Event, 1, &Timeout); + + int ExitCode = -1; + int Status = 0; + if (waitpid(m_Pid, &Status, WNOHANG) > 0) + { + if (WIFEXITED(Status)) + { + ExitCode = WEXITSTATUS(Status); + } + else if (WIFSIGNALED(Status)) + { + constexpr int kSignalExitBase = 128; + ExitCode = kSignalExitBase + WTERMSIG(Status); + } + } + + Callback(ExitCode); + }); + } + + void Cancel() + { + if (m_Descriptor) + { + asio::error_code Ec; + m_Descriptor->cancel(Ec); + m_Descriptor.reset(); + // stream_descriptor closes the kqueue fd on destruction + m_KqueueFd = -1; + } + else if (m_KqueueFd >= 0) + { + close(m_KqueueFd); + m_KqueueFd = -1; + } + } +}; + +#endif + +// ============================================================================ +// Common wrapper (delegates to Impl) +// ============================================================================ + +ProcessExitWatcher::ProcessExitWatcher(asio::io_context& IoContext) : m_Impl(std::make_unique(IoContext)) +{ +} + +ProcessExitWatcher::~ProcessExitWatcher() = default; + +void +ProcessExitWatcher::Watch(const ProcessHandle& Handle, std::function OnExit) +{ + m_Impl->Watch(Handle, std::move(OnExit)); +} + +void +ProcessExitWatcher::Cancel() +{ + m_Impl->Cancel(); +} + +} // namespace zen -- cgit v1.2.3