aboutsummaryrefslogtreecommitdiff
path: root/src/zencore
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/zencore
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/zencore')
-rw-r--r--src/zencore/include/zencore/process.h49
-rw-r--r--src/zencore/logging.cpp7
-rw-r--r--src/zencore/process.cpp53
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);