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/zencore | |
| 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/zencore')
| -rw-r--r-- | src/zencore/include/zencore/process.h | 49 | ||||
| -rw-r--r-- | src/zencore/logging.cpp | 7 | ||||
| -rw-r--r-- | src/zencore/process.cpp | 53 |
3 files changed, 101 insertions, 8 deletions
diff --git a/src/zencore/include/zencore/process.h b/src/zencore/include/zencore/process.h index d115bf11f..5ae7fad68 100644 --- a/src/zencore/include/zencore/process.h +++ b/src/zencore/include/zencore/process.h @@ -29,9 +29,23 @@ class ProcessHandle public: ProcessHandle(); + /// Construct by opening a handle to the process identified by @p Pid. + /// On Windows this calls OpenProcess(); on POSIX it simply stores the pid. + /// Throws std::system_error on failure. + explicit ProcessHandle(int Pid); + + /// Construct from an existing native process handle. Takes ownership — + /// the caller must not close the handle afterwards. Windows only. +#if ZEN_PLATFORM_WINDOWS + explicit ProcessHandle(void* NativeHandle); +#endif + ProcessHandle(const ProcessHandle&) = delete; ProcessHandle& operator=(const ProcessHandle&) = delete; + ProcessHandle(ProcessHandle&& Other) noexcept; + ProcessHandle& operator=(ProcessHandle&& Other) noexcept; + ~ProcessHandle(); /// Open a handle to the process identified by @p Pid. @@ -44,7 +58,9 @@ public: /// Initialize from an existing native process handle. Takes ownership — /// the caller must not close the handle afterwards. Windows only. +#if ZEN_PLATFORM_WINDOWS void Initialize(void* ProcessHandle); +#endif /// Returns true if the process is still alive. /// On Windows, queries the exit code (STILL_ACTIVE check). @@ -148,14 +164,23 @@ struct CreateProcOptions { enum { + // Allocate a new console for the child (CREATE_NEW_CONSOLE on Windows). Flag_NewConsole = 1 << 0, - Flag_Elevated = 1 << 1, + // Launch the child with elevated (administrator) privileges via ShellExecuteEx/runas. + Flag_Elevated = 1 << 1, + // Launch the child without elevation from an elevated parent, using a medium-integrity token. Flag_Unelevated = 1 << 2, - Flag_NoConsole = 1 << 3, - // This flag creates the new process in a new process group. This is relevant only on Windows, and - // allows sending ctrl-break events to the new process group without also sending it to the current - // process. + // Detach the child from all consoles (DETACHED_PROCESS on Windows). No console is + // allocated and no conhost.exe is spawned. Stdout/stderr still work when redirected + // via pipes. Prefer this for headless worker processes. + Flag_NoConsole = 1 << 3, + // Create the child in a new process group (CREATE_NEW_PROCESS_GROUP on Windows). + // Allows sending CTRL_BREAK_EVENT to the child group without affecting the parent. Flag_Windows_NewProcessGroup = 1 << 4, + // Allocate a hidden console for the child (CREATE_NO_WINDOW on Windows). Unlike + // Flag_NoConsole the child still gets a console (and a conhost.exe) but no visible + // window. Use this when the child needs a console for stdio but should not show a window. + Flag_NoWindow = 1 << 5, }; const std::filesystem::path* WorkingDirectory = nullptr; @@ -171,9 +196,16 @@ struct CreateProcOptions #if ZEN_PLATFORM_WINDOWS JobObject* AssignToJob = nullptr; // When set, the process is created suspended, assigned to the job, then resumed +#else + /// POSIX process group id. When > 0, the child is placed into this process + /// group via setpgid() before exec. Use the pid of the first child as the + /// pgid to create a group, then pass the same pgid for subsequent children. + int ProcessGroupId = 0; #endif }; +// TODO: this should really be replaced with ProcessHandle + #if ZEN_PLATFORM_WINDOWS using CreateProcResult = void*; // handle to the process #else @@ -224,9 +256,10 @@ public: JobObject(const JobObject&) = delete; JobObject& operator=(const JobObject&) = delete; - void Initialize(); - bool AssignProcess(void* ProcessHandle); - [[nodiscard]] bool IsValid() const; + void Initialize(); + bool AssignProcess(void* ProcessHandle); + [[nodiscard]] bool IsValid() const; + [[nodiscard]] void* Handle() const { return m_JobHandle; } private: void* m_JobHandle = nullptr; diff --git a/src/zencore/logging.cpp b/src/zencore/logging.cpp index 828bea6ed..5ada0cac7 100644 --- a/src/zencore/logging.cpp +++ b/src/zencore/logging.cpp @@ -414,6 +414,13 @@ InitializeLogging() { ZEN_MEMSCOPE(ELLMTag::Logging); + EnableVTMode(); + +#if ZEN_PLATFORM_WINDOWS + // Enable UTF-8 output so multi-byte characters render correctly via WriteFile + SetConsoleOutputCP(CP_UTF8); +#endif + TheDefaultLogger = LoggerRef(*Registry::Instance().DefaultLoggerRaw()); g_LoggingInitialized = true; } diff --git a/src/zencore/process.cpp b/src/zencore/process.cpp index dcb8b2422..e7baa3f8e 100644 --- a/src/zencore/process.cpp +++ b/src/zencore/process.cpp @@ -277,6 +277,46 @@ CreateStdoutPipe(StdoutPipeHandles& OutPipe) ProcessHandle::ProcessHandle() = default; +ProcessHandle::ProcessHandle(int Pid) +{ + Initialize(Pid); +} + +#if ZEN_PLATFORM_WINDOWS +ProcessHandle::ProcessHandle(void* NativeHandle) +{ + Initialize(NativeHandle); +} +#endif + +ProcessHandle::ProcessHandle(ProcessHandle&& Other) noexcept +: m_ProcessHandle(Other.m_ProcessHandle) +, m_Pid(Other.m_Pid) +#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC +, m_ExitCode(Other.m_ExitCode) +#endif +{ + Other.m_ProcessHandle = nullptr; + Other.m_Pid = 0; +} + +ProcessHandle& +ProcessHandle::operator=(ProcessHandle&& Other) noexcept +{ + if (this != &Other) + { + Reset(); + m_ProcessHandle = Other.m_ProcessHandle; + m_Pid = Other.m_Pid; +#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + m_ExitCode = Other.m_ExitCode; +#endif + Other.m_ProcessHandle = nullptr; + Other.m_Pid = 0; + } + return *this; +} + #if ZEN_PLATFORM_WINDOWS void ProcessHandle::Initialize(void* ProcessHandle) @@ -720,6 +760,10 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma } if (Options.Flags & CreateProcOptions::Flag_NoConsole) { + CreationFlags |= DETACHED_PROCESS; + } + if (Options.Flags & CreateProcOptions::Flag_NoWindow) + { CreationFlags |= CREATE_NO_WINDOW; } if (Options.Flags & CreateProcOptions::Flag_Windows_NewProcessGroup) @@ -930,6 +974,10 @@ CreateProcUnelevated(const std::filesystem::path& Executable, std::string_view C } if (Options.Flags & CreateProcOptions::Flag_NoConsole) { + CreateProcFlags |= DETACHED_PROCESS; + } + if (Options.Flags & CreateProcOptions::Flag_NoWindow) + { CreateProcFlags |= CREATE_NO_WINDOW; } if (AssignToJob) @@ -1070,6 +1118,11 @@ CreateProc(const std::filesystem::path& Executable, std::string_view CommandLine } } + if (Options.ProcessGroupId > 0) + { + setpgid(0, Options.ProcessGroupId); + } + for (const auto& [Key, Value] : Options.Environment) { setenv(Key.c_str(), Value.c_str(), 1); |