aboutsummaryrefslogtreecommitdiff
path: root/src/zenutil/process/exitwatcher.cpp
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/process/exitwatcher.cpp
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/process/exitwatcher.cpp')
-rw-r--r--src/zenutil/process/exitwatcher.cpp294
1 files changed, 294 insertions, 0 deletions
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 <zencore/logging.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+
+#if ZEN_PLATFORM_WINDOWS
+# include <zencore/windows.h>
+# include <asio/io_context.hpp>
+# include <asio/windows/object_handle.hpp>
+#elif ZEN_PLATFORM_LINUX
+# include <sys/syscall.h>
+# include <sys/wait.h>
+# include <unistd.h>
+# include <asio/io_context.hpp>
+# include <asio/posix/stream_descriptor.hpp>
+
+# ifndef SYS_pidfd_open
+# define SYS_pidfd_open 434 // x86_64
+# endif
+#elif ZEN_PLATFORM_MAC
+# include <sys/event.h>
+# include <sys/wait.h>
+# include <unistd.h>
+# include <asio/io_context.hpp>
+# include <asio/posix/stream_descriptor.hpp>
+#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<asio::posix::stream_descriptor> 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<void(int ExitCode)> 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<int>(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<asio::posix::stream_descriptor>(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<asio::windows::object_handle> m_ObjectHandle;
+ void* m_DuplicatedHandle = nullptr;
+
+ explicit Impl(asio::io_context& IoContext) : m_IoContext(IoContext) {}
+
+ ~Impl() { Cancel(); }
+
+ void Watch(const ProcessHandle& Handle, std::function<void(int ExitCode)> OnExit)
+ {
+ // Duplicate the process handle so ASIO can take ownership independently
+ HANDLE SourceHandle = static_cast<HANDLE>(Handle.Handle());
+ HANDLE CurrentProcess = GetCurrentProcess();
+ BOOL Success = DuplicateHandle(CurrentProcess,
+ SourceHandle,
+ CurrentProcess,
+ reinterpret_cast<LPHANDLE>(&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<asio::windows::object_handle>(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<HANDLE>(DupHandle), &ExitCode);
+ Callback(static_cast<int>(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<HANDLE>(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<asio::posix::stream_descriptor> 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<void(int ExitCode)> 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<uintptr_t>(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<asio::posix::stream_descriptor>(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<Impl>(IoContext))
+{
+}
+
+ProcessExitWatcher::~ProcessExitWatcher() = default;
+
+void
+ProcessExitWatcher::Watch(const ProcessHandle& Handle, std::function<void(int ExitCode)> OnExit)
+{
+ m_Impl->Watch(Handle, std::move(OnExit));
+}
+
+void
+ProcessExitWatcher::Cancel()
+{
+ m_Impl->Cancel();
+}
+
+} // namespace zen