aboutsummaryrefslogtreecommitdiff
path: root/src/zencore/process.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zencore/process.cpp')
-rw-r--r--src/zencore/process.cpp713
1 files changed, 658 insertions, 55 deletions
diff --git a/src/zencore/process.cpp b/src/zencore/process.cpp
index 0c55e6c7e..ee821944a 100644
--- a/src/zencore/process.cpp
+++ b/src/zencore/process.cpp
@@ -37,7 +37,9 @@ ZEN_THIRD_PARTY_INCLUDES_START
#endif
#if ZEN_PLATFORM_MAC
+# include <crt_externs.h>
# include <libproc.h>
+# include <spawn.h>
# include <sys/types.h>
# include <sys/sysctl.h>
#endif
@@ -135,10 +137,248 @@ IsZombieProcess(int pid, std::error_code& OutEc)
}
return false;
}
+
+static char**
+GetEnviron()
+{
+ return *_NSGetEnviron();
+}
#endif // ZEN_PLATFORM_MAC
+#if ZEN_PLATFORM_LINUX
+static char**
+GetEnviron()
+{
+ return environ;
+}
+#endif // ZEN_PLATFORM_LINUX
+
+#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
+// Holds a null-terminated envp array built by merging the current process environment with
+// a set of overrides. When Overrides is empty, Data points directly to environ (no allocation).
+// Must outlive any posix_spawn / execve call that receives Data.
+struct EnvpHolder
+{
+ char** Data = GetEnviron();
+
+ explicit EnvpHolder(const std::vector<std::pair<std::string, std::string>>& Overrides)
+ {
+ if (Overrides.empty())
+ {
+ return;
+ }
+ std::map<std::string, std::string> EnvMap;
+ for (char** E = GetEnviron(); *E; ++E)
+ {
+ std::string_view Entry(*E);
+ const size_t EqPos = Entry.find('=');
+ if (EqPos != std::string_view::npos)
+ {
+ EnvMap[std::string(Entry.substr(0, EqPos))] = std::string(Entry.substr(EqPos + 1));
+ }
+ }
+ for (const auto& [Key, Value] : Overrides)
+ {
+ EnvMap[Key] = Value;
+ }
+ for (const auto& [Key, Value] : EnvMap)
+ {
+ m_Strings.push_back(Key + "=" + Value);
+ }
+ for (std::string& S : m_Strings)
+ {
+ m_Ptrs.push_back(S.data());
+ }
+ m_Ptrs.push_back(nullptr);
+ Data = m_Ptrs.data();
+ }
+
+private:
+ std::vector<std::string> m_Strings;
+ std::vector<char*> m_Ptrs;
+};
+#endif // ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
+
+//////////////////////////////////////////////////////////////////////////
+// Pipe creation for child process stdout capture
+
+#if ZEN_PLATFORM_WINDOWS
+
+StdoutPipeHandles::~StdoutPipeHandles()
+{
+ Close();
+}
+
+StdoutPipeHandles::StdoutPipeHandles(StdoutPipeHandles&& Other) noexcept
+: ReadHandle(std::exchange(Other.ReadHandle, nullptr))
+, WriteHandle(std::exchange(Other.WriteHandle, nullptr))
+{
+}
+
+StdoutPipeHandles&
+StdoutPipeHandles::operator=(StdoutPipeHandles&& Other) noexcept
+{
+ if (this != &Other)
+ {
+ Close();
+ ReadHandle = std::exchange(Other.ReadHandle, nullptr);
+ WriteHandle = std::exchange(Other.WriteHandle, nullptr);
+ }
+ return *this;
+}
+
+void
+StdoutPipeHandles::CloseWriteEnd()
+{
+ if (WriteHandle)
+ {
+ CloseHandle(WriteHandle);
+ WriteHandle = nullptr;
+ }
+}
+
+void
+StdoutPipeHandles::Close()
+{
+ if (ReadHandle)
+ {
+ CloseHandle(ReadHandle);
+ ReadHandle = nullptr;
+ }
+ CloseWriteEnd();
+}
+
+bool
+CreateStdoutPipe(StdoutPipeHandles& OutPipe)
+{
+ SECURITY_ATTRIBUTES Sa;
+ Sa.nLength = sizeof(Sa);
+ Sa.lpSecurityDescriptor = nullptr;
+ Sa.bInheritHandle = TRUE;
+
+ HANDLE ReadHandle = nullptr;
+ HANDLE WriteHandle = nullptr;
+ if (!::CreatePipe(&ReadHandle, &WriteHandle, &Sa, 0))
+ {
+ return false;
+ }
+
+ // The read end should not be inherited by the child
+ SetHandleInformation(ReadHandle, HANDLE_FLAG_INHERIT, 0);
+
+ OutPipe.ReadHandle = ReadHandle;
+ OutPipe.WriteHandle = WriteHandle;
+ return true;
+}
+
+#else
+
+StdoutPipeHandles::~StdoutPipeHandles()
+{
+ Close();
+}
+
+StdoutPipeHandles::StdoutPipeHandles(StdoutPipeHandles&& Other) noexcept
+: ReadFd(std::exchange(Other.ReadFd, -1))
+, WriteFd(std::exchange(Other.WriteFd, -1))
+{
+}
+
+StdoutPipeHandles&
+StdoutPipeHandles::operator=(StdoutPipeHandles&& Other) noexcept
+{
+ if (this != &Other)
+ {
+ Close();
+ ReadFd = std::exchange(Other.ReadFd, -1);
+ WriteFd = std::exchange(Other.WriteFd, -1);
+ }
+ return *this;
+}
+
+void
+StdoutPipeHandles::CloseWriteEnd()
+{
+ if (WriteFd >= 0)
+ {
+ close(WriteFd);
+ WriteFd = -1;
+ }
+}
+
+void
+StdoutPipeHandles::Close()
+{
+ if (ReadFd >= 0)
+ {
+ close(ReadFd);
+ ReadFd = -1;
+ }
+ CloseWriteEnd();
+}
+
+bool
+CreateStdoutPipe(StdoutPipeHandles& OutPipe)
+{
+ int Fds[2];
+ if (pipe(Fds) != 0)
+ {
+ return false;
+ }
+ OutPipe.ReadFd = Fds[0];
+ OutPipe.WriteFd = Fds[1];
+
+ // Set close-on-exec on the read end so the child doesn't inherit it
+ fcntl(OutPipe.ReadFd, F_SETFD, FD_CLOEXEC);
+ return true;
+}
+
+#endif
+
+//////////////////////////////////////////////////////////////////////////
+
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)
@@ -259,6 +499,17 @@ ProcessHandle::Kill()
return false;
}
}
+
+ // Wait for the process to exit after SIGTERM, matching the Windows path
+ // which waits up to 5 seconds for graceful shutdown. Without this wait
+ // the child becomes a zombie and may hold resources (e.g. TCP ports).
+ std::error_code Ec;
+ if (!Wait(5000, Ec))
+ {
+ // Graceful shutdown timed out — force-kill
+ kill(pid_t(m_Pid), SIGKILL);
+ Wait(1000, Ec);
+ }
#endif
Reset();
@@ -297,6 +548,11 @@ ProcessHandle::Terminate(int ExitCode)
return false;
}
}
+
+ // Wait for the process to be reaped after SIGKILL so it doesn't linger
+ // as a zombie holding resources (e.g. TCP ports).
+ std::error_code Ec;
+ Wait(5000, Ec);
#endif
Reset();
return true;
@@ -309,6 +565,10 @@ ProcessHandle::Reset()
{
#if ZEN_PLATFORM_WINDOWS
CloseHandle(m_ProcessHandle);
+#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
+ // Reap the child if it has already exited to prevent zombies.
+ // If still running, it will be reparented to init on our exit.
+ waitpid(m_Pid, nullptr, WNOHANG);
#endif
m_ProcessHandle = nullptr;
m_Pid = 0;
@@ -350,17 +610,26 @@ ProcessHandle::Wait(int TimeoutMs, std::error_code& OutEc)
timespec SleepTime = {0, SleepMs * 1000 * 1000};
for (int SleepedTimeMS = 0;; SleepedTimeMS += SleepMs)
{
- int WaitState = 0;
- if (waitpid(m_Pid, &WaitState, WNOHANG | WCONTINUED | WUNTRACED) != -1)
+ int WaitState = 0;
+ pid_t WaitResult = waitpid(m_Pid, &WaitState, WNOHANG | WCONTINUED | WUNTRACED);
+ if (WaitResult > 0 && WIFEXITED(WaitState))
{
- if (WIFEXITED(WaitState))
- {
- m_ExitCode = WEXITSTATUS(WaitState);
- }
+ m_ExitCode = WEXITSTATUS(WaitState);
}
if (!IsProcessRunning(m_Pid, OutEc))
{
+ // Process is gone but waitpid(WNOHANG) may have missed the exit status
+ // due to a TOCTOU race (process became a zombie between waitpid and
+ // IsProcessRunning). Do a blocking reap now to capture the exit code.
+ if (WaitResult <= 0)
+ {
+ WaitState = 0;
+ if (waitpid(m_Pid, &WaitState, 0) > 0 && WIFEXITED(WaitState))
+ {
+ m_ExitCode = WEXITSTATUS(WaitState);
+ }
+ }
return true;
}
else if (OutEc)
@@ -381,6 +650,12 @@ ProcessHandle::Wait(int TimeoutMs, std::error_code& OutEc)
else if (IsZombieProcess(m_Pid, OutEc))
{
ZEN_INFO("Found process {} in zombie state, treating as not running", m_Pid);
+ // Reap the zombie to capture its exit code.
+ WaitState = 0;
+ if (waitpid(m_Pid, &WaitState, 0) > 0 && WIFEXITED(WaitState))
+ {
+ m_ExitCode = WEXITSTATUS(WaitState);
+ }
return true;
}
@@ -478,6 +753,7 @@ BuildArgV(std::vector<char*>& Out, char* CommandLine)
++Cursor;
}
}
+
#endif // !WINDOWS || TESTS
#if ZEN_PLATFORM_WINDOWS
@@ -547,9 +823,13 @@ 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)
+ if (Options.Flags & CreateProcOptions::Flag_NewProcessGroup)
{
CreationFlags |= CREATE_NEW_PROCESS_GROUP;
}
@@ -567,7 +847,40 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
ExtendableWideStringBuilder<256> CommandLineZ;
CommandLineZ << CommandLine;
- if (!Options.StdoutFile.empty())
+ bool DuplicatedStdErr = false;
+
+ if (Options.StdoutPipe != nullptr && Options.StdoutPipe->WriteHandle != nullptr)
+ {
+ StartupInfo.hStdInput = nullptr;
+ StartupInfo.hStdOutput = (HANDLE)Options.StdoutPipe->WriteHandle;
+
+ if (Options.StderrPipe != nullptr && Options.StderrPipe->WriteHandle != nullptr)
+ {
+ // Use separate pipe for stderr
+ StartupInfo.hStdError = (HANDLE)Options.StderrPipe->WriteHandle;
+ StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
+ InheritHandles = true;
+ }
+ else
+ {
+ // Duplicate stdout handle for stderr (both go to same pipe)
+ const BOOL DupSuccess = DuplicateHandle(GetCurrentProcess(),
+ StartupInfo.hStdOutput,
+ GetCurrentProcess(),
+ &StartupInfo.hStdError,
+ 0,
+ TRUE,
+ DUPLICATE_SAME_ACCESS);
+
+ if (DupSuccess)
+ {
+ DuplicatedStdErr = true;
+ StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
+ InheritHandles = true;
+ }
+ }
+ }
+ else if (!Options.StdoutFile.empty())
{
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof sa;
@@ -593,6 +906,7 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
if (Success)
{
+ DuplicatedStdErr = true;
StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
InheritHandles = true;
}
@@ -616,8 +930,16 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
if (StartupInfo.dwFlags & STARTF_USESTDHANDLES)
{
- CloseHandle(StartupInfo.hStdError);
- CloseHandle(StartupInfo.hStdOutput);
+ // Only close hStdError if we duplicated it (caller-owned pipe handles are not ours to close)
+ if (DuplicatedStdErr)
+ {
+ CloseHandle(StartupInfo.hStdError);
+ }
+ // Only close hStdOutput if it was a file handle we created (not a pipe handle owned by caller)
+ if (Options.StdoutPipe == nullptr || Options.StdoutPipe->WriteHandle == nullptr)
+ {
+ CloseHandle(StartupInfo.hStdOutput);
+ }
}
if (!Success)
@@ -715,6 +1037,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)
@@ -807,26 +1133,51 @@ CreateProc(const std::filesystem::path& Executable, std::string_view CommandLine
}
return CreateProcNormal(Executable, CommandLine, Options);
-#else
+#elif ZEN_PLATFORM_LINUX
+ // vfork uses CLONE_VM|CLONE_VFORK: the child shares the parent's address space and the
+ // parent is suspended until the child calls exec or _exit. This avoids page-table duplication
+ // and the ENOMEM that fork() produces on systems with strict overcommit (vm.overcommit_memory=2).
+ // All child-side setup uses only syscalls that do not modify user-space memory.
+ // Environment overrides are merged into envp before vfork so that setenv() is never called
+ // from the child (which would corrupt the shared address space).
std::vector<char*> ArgV;
std::string CommandLineZ(CommandLine);
BuildArgV(ArgV, CommandLineZ.data());
ArgV.push_back(nullptr);
- int ChildPid = fork();
+ EnvpHolder Envp(Options.Environment);
+
+ int ChildPid = vfork();
if (ChildPid < 0)
{
- ThrowLastError("Failed to fork a new child process");
+ ThrowLastError("Failed to vfork a new child process");
}
else if (ChildPid == 0)
{
if (Options.WorkingDirectory != nullptr)
{
- int Result = chdir(Options.WorkingDirectory->c_str());
- ZEN_UNUSED(Result);
+ chdir(Options.WorkingDirectory->c_str());
}
- if (!Options.StdoutFile.empty())
+ if (Options.StdoutPipe != nullptr && Options.StdoutPipe->WriteFd >= 0)
+ {
+ dup2(Options.StdoutPipe->WriteFd, STDOUT_FILENO);
+
+ if (Options.StderrPipe != nullptr && Options.StderrPipe->WriteFd >= 0)
+ {
+ dup2(Options.StderrPipe->WriteFd, STDERR_FILENO);
+ close(Options.StderrPipe->WriteFd);
+ // StderrPipe ReadFd has FD_CLOEXEC so it's auto-closed on exec
+ }
+ else
+ {
+ dup2(Options.StdoutPipe->WriteFd, STDERR_FILENO);
+ }
+
+ close(Options.StdoutPipe->WriteFd);
+ // ReadFd has FD_CLOEXEC so it's auto-closed on exec
+ }
+ else if (!Options.StdoutFile.empty())
{
int Fd = open(Options.StdoutFile.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (Fd >= 0)
@@ -837,18 +1188,99 @@ CreateProc(const std::filesystem::path& Executable, std::string_view CommandLine
}
}
- for (const auto& [Key, Value] : Options.Environment)
+ if (Options.Flags & CreateProcOptions::Flag_NewProcessGroup)
{
- setenv(Key.c_str(), Value.c_str(), 1);
+ setpgid(0, 0);
}
-
- if (execv(Executable.c_str(), ArgV.data()) < 0)
+ else if (Options.ProcessGroupId > 0)
{
- ThrowLastError("Failed to exec() a new process image");
+ setpgid(0, Options.ProcessGroupId);
}
+
+ execve(Executable.c_str(), ArgV.data(), Envp.Data);
+ _exit(127);
}
return ChildPid;
+#else // macOS
+ std::vector<char*> ArgV;
+ std::string CommandLineZ(CommandLine);
+ BuildArgV(ArgV, CommandLineZ.data());
+ ArgV.push_back(nullptr);
+
+ posix_spawn_file_actions_t FileActions;
+ posix_spawnattr_t Attr;
+
+ int Err = posix_spawn_file_actions_init(&FileActions);
+ if (Err != 0)
+ {
+ ThrowSystemError(Err, "posix_spawn_file_actions_init failed");
+ }
+ auto FileActionsGuard = MakeGuard([&] { posix_spawn_file_actions_destroy(&FileActions); });
+
+ Err = posix_spawnattr_init(&Attr);
+ if (Err != 0)
+ {
+ ThrowSystemError(Err, "posix_spawnattr_init failed");
+ }
+ auto AttrGuard = MakeGuard([&] { posix_spawnattr_destroy(&Attr); });
+
+ if (Options.WorkingDirectory != nullptr)
+ {
+ Err = posix_spawn_file_actions_addchdir_np(&FileActions, Options.WorkingDirectory->c_str());
+ if (Err != 0)
+ {
+ ThrowSystemError(Err, "posix_spawn_file_actions_addchdir_np failed");
+ }
+ }
+
+ if (Options.StdoutPipe != nullptr && Options.StdoutPipe->WriteFd >= 0)
+ {
+ const int StdoutWriteFd = Options.StdoutPipe->WriteFd;
+ ZEN_ASSERT(StdoutWriteFd > STDERR_FILENO);
+ posix_spawn_file_actions_adddup2(&FileActions, StdoutWriteFd, STDOUT_FILENO);
+
+ if (Options.StderrPipe != nullptr && Options.StderrPipe->WriteFd >= 0)
+ {
+ const int StderrWriteFd = Options.StderrPipe->WriteFd;
+ ZEN_ASSERT(StderrWriteFd > STDERR_FILENO && StderrWriteFd != StdoutWriteFd);
+ posix_spawn_file_actions_adddup2(&FileActions, StderrWriteFd, STDERR_FILENO);
+ posix_spawn_file_actions_addclose(&FileActions, StderrWriteFd);
+ }
+ else
+ {
+ posix_spawn_file_actions_adddup2(&FileActions, StdoutWriteFd, STDERR_FILENO);
+ }
+
+ posix_spawn_file_actions_addclose(&FileActions, StdoutWriteFd);
+ }
+ else if (!Options.StdoutFile.empty())
+ {
+ posix_spawn_file_actions_addopen(&FileActions, STDOUT_FILENO, Options.StdoutFile.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ posix_spawn_file_actions_adddup2(&FileActions, STDOUT_FILENO, STDERR_FILENO);
+ }
+
+ if (Options.Flags & CreateProcOptions::Flag_NewProcessGroup)
+ {
+ posix_spawnattr_setflags(&Attr, POSIX_SPAWN_SETPGROUP);
+ posix_spawnattr_setpgroup(&Attr, 0);
+ }
+ else if (Options.ProcessGroupId > 0)
+ {
+ posix_spawnattr_setflags(&Attr, POSIX_SPAWN_SETPGROUP);
+ posix_spawnattr_setpgroup(&Attr, Options.ProcessGroupId);
+ }
+
+ EnvpHolder Envp(Options.Environment);
+
+ pid_t ChildPid = 0;
+ Err = posix_spawn(&ChildPid, Executable.c_str(), &FileActions, &Attr, ArgV.data(), Envp.Data);
+ if (Err != 0)
+ {
+ ThrowSystemError(Err, "Failed to posix_spawn a new child process");
+ }
+
+ return int(ChildPid);
#endif
}
@@ -966,14 +1398,28 @@ JobObject::Initialize()
}
JOBOBJECT_EXTENDED_LIMIT_INFORMATION LimitInfo = {};
- LimitInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
+ LimitInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION;
if (!SetInformationJobObject(m_JobHandle, JobObjectExtendedLimitInformation, &LimitInfo, sizeof(LimitInfo)))
{
ZEN_WARN("Failed to set job object limits: {}", zen::GetLastError());
CloseHandle(m_JobHandle);
m_JobHandle = nullptr;
+ return;
}
+
+ // Prevent child processes from clearing SEM_NOGPFAULTERRORBOX, which
+ // suppresses WER/Dr. Watson crash dialogs. Without this, a crashing
+ // child can pop a modal dialog and block the monitor thread.
+# if !defined(JOB_OBJECT_UILIMIT_ERRORMODE)
+# define JOB_OBJECT_UILIMIT_ERRORMODE 0x00000400
+# endif
+ JOBOBJECT_BASIC_UI_RESTRICTIONS UiRestrictions{};
+ UiRestrictions.UIRestrictionsClass = JOB_OBJECT_UILIMIT_ERRORMODE;
+ SetInformationJobObject(m_JobHandle, JobObjectBasicUIRestrictions, &UiRestrictions, sizeof(UiRestrictions));
+
+ // Set error mode on the current process so children inherit it.
+ SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
}
bool
@@ -1431,47 +1877,60 @@ FindProcess(const std::filesystem::path& ExecutableImage, ProcessHandle& OutHand
return MakeErrorCodeFromLastError();
#endif // ZEN_PLATFORM_WINDOWS
#if ZEN_PLATFORM_MAC
- int Mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0};
- size_t BufferSize = 0;
-
- struct kinfo_proc* Processes = nullptr;
- uint32_t ProcCount = 0;
+ int Mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0};
const pid_t ThisProcessId = getpid();
- if (sysctl(Mib, 4, NULL, &BufferSize, NULL, 0) != -1 && BufferSize > 0)
+ // The process list can change between the sizing sysctl call and the data sysctl call.
+ // Retry with padding to handle this race.
+ struct kinfo_proc* Processes = nullptr;
+ size_t BufferSize = 0;
+ bool Fetched = false;
+ auto _ = MakeGuard([&]() { free(Processes); });
+
+ for (int Attempt = 0; Attempt < 3; Attempt++)
+ {
+ if (sysctl(Mib, 4, nullptr, &BufferSize, nullptr, 0) == -1 || BufferSize == 0)
+ {
+ break;
+ }
+ BufferSize += BufferSize / 4;
+ free(Processes);
+ Processes = (struct kinfo_proc*)malloc(BufferSize);
+ if (sysctl(Mib, 4, Processes, &BufferSize, nullptr, 0) != -1)
+ {
+ Fetched = true;
+ break;
+ }
+ }
+
+ if (!Fetched)
+ {
+ return MakeErrorCodeFromLastError();
+ }
+
+ uint32_t ProcCount = (uint32_t)(BufferSize / sizeof(struct kinfo_proc));
+ for (uint32_t ProcIndex = 0; ProcIndex < ProcCount; ProcIndex++)
{
- struct kinfo_proc* Processes = (struct kinfo_proc*)malloc(BufferSize);
- auto _ = MakeGuard([&]() { free(Processes); });
- if (sysctl(Mib, 4, Processes, &BufferSize, NULL, 0) != -1)
+ pid_t Pid = Processes[ProcIndex].kp_proc.p_pid;
+ if (IncludeSelf || (Pid != ThisProcessId))
{
- ProcCount = (uint32_t)(BufferSize / sizeof(struct kinfo_proc));
- char Buffer[PROC_PIDPATHINFO_MAXSIZE];
- for (uint32_t ProcIndex = 0; ProcIndex < ProcCount; ProcIndex++)
+ std::error_code Ec;
+ std::filesystem::path EntryPath = GetProcessExecutablePath(Pid, Ec);
+ if (!Ec)
{
- pid_t Pid = Processes[ProcIndex].kp_proc.p_pid;
- if (IncludeSelf || (Pid != ThisProcessId))
+ if (EntryPath == ExecutableImage)
{
- std::error_code Ec;
- std::filesystem::path EntryPath = GetProcessExecutablePath(Pid, Ec);
- if (!Ec)
+ if (Processes[ProcIndex].kp_proc.p_stat != SZOMB)
{
- if (EntryPath == ExecutableImage)
- {
- if (Processes[ProcIndex].kp_proc.p_stat != SZOMB)
- {
- OutHandle.Initialize(Pid, Ec);
- return Ec;
- }
- }
+ OutHandle.Initialize(Pid, Ec);
+ return Ec;
}
- Ec.clear();
}
}
- return {};
}
}
- return MakeErrorCodeFromLastError();
+ return {};
#endif // ZEN_PLATFORM_MAC
#if ZEN_PLATFORM_LINUX
const pid_t ThisProcessId = getpid();
@@ -1564,7 +2023,7 @@ WaitForThreads(uint64_t WaitTimeMs)
}
void
-GetProcessMetrics(ProcessHandle& Handle, ProcessMetrics& OutMetrics)
+GetProcessMetrics(const ProcessHandle& Handle, ProcessMetrics& OutMetrics)
{
#if ZEN_PLATFORM_WINDOWS
FILETIME CreationTime;
@@ -1593,11 +2052,137 @@ GetProcessMetrics(ProcessHandle& Handle, ProcessMetrics& OutMetrics)
OutMetrics.PeakWorkingSetSize = MemCounters.PeakWorkingSetSize;
OutMetrics.PagefileUsage = MemCounters.PagefileUsage;
OutMetrics.PeakPagefileUsage = MemCounters.PeakPagefileUsage;
+ OutMetrics.MemoryBytes = MemCounters.WorkingSetSize;
}
-#else
- // TODO: implement for Linux and Mac
- ZEN_UNUSED(Handle);
- ZEN_UNUSED(OutMetrics);
+#elif ZEN_PLATFORM_LINUX
+
+ const pid_t Pid = static_cast<pid_t>(Handle.Pid());
+
+ // Read CPU times from /proc/{pid}/stat
+ {
+ char Path[64];
+ snprintf(Path, sizeof(Path), "/proc/%d/stat", static_cast<int>(Pid));
+
+ char Buf[256];
+ int Fd = open(Path, O_RDONLY);
+ if (Fd >= 0)
+ {
+ ssize_t Len = read(Fd, Buf, sizeof(Buf) - 1);
+ close(Fd);
+
+ if (Len > 0)
+ {
+ Buf[Len] = '\0';
+
+ // Skip past "pid (name) " — find last ')' to handle names containing spaces or parens
+ const char* P = strrchr(Buf, ')');
+ if (P)
+ {
+ P += 2; // skip ') '
+
+ // Fields after (name): 0:state 1:ppid ... 11:utime 12:stime
+ unsigned long UTime = 0;
+ unsigned long STime = 0;
+ if (sscanf(P, "%*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %lu %lu", &UTime, &STime) == 2)
+ {
+ static const long ClkTck = std::max(sysconf(_SC_CLK_TCK), 1L);
+ OutMetrics.KernelTimeMs = STime * 1000 / ClkTck;
+ OutMetrics.UserTimeMs = UTime * 1000 / ClkTck;
+ }
+ }
+ }
+ }
+ }
+
+ // Read memory metrics from /proc/{pid}/statm (values in pages)
+ {
+ char Path[64];
+ snprintf(Path, sizeof(Path), "/proc/%d/statm", static_cast<int>(Pid));
+
+ char Buf[128];
+ int Fd = open(Path, O_RDONLY);
+ if (Fd >= 0)
+ {
+ ssize_t Len = read(Fd, Buf, sizeof(Buf) - 1);
+ close(Fd);
+
+ if (Len > 0)
+ {
+ Buf[Len] = '\0';
+
+ // Fields: size resident shared text lib data dt
+ unsigned long VmSize = 0;
+ unsigned long Resident = 0;
+ if (sscanf(Buf, "%lu %lu", &VmSize, &Resident) == 2)
+ {
+ static const long PageSize = sysconf(_SC_PAGESIZE);
+ OutMetrics.WorkingSetSize = Resident * PageSize;
+ OutMetrics.PagefileUsage = VmSize * PageSize;
+ }
+ }
+ }
+ }
+
+ // Read peak RSS from /proc/{pid}/status (VmHWM line)
+ {
+ char Path[64];
+ snprintf(Path, sizeof(Path), "/proc/%d/status", static_cast<int>(Pid));
+
+ char Buf[2048];
+ int Fd = open(Path, O_RDONLY);
+ if (Fd >= 0)
+ {
+ ssize_t Len = read(Fd, Buf, sizeof(Buf) - 1);
+ close(Fd);
+
+ if (Len > 0)
+ {
+ Buf[Len] = '\0';
+
+ const char* VmHWM = strstr(Buf, "VmHWM:");
+ if (VmHWM)
+ {
+ unsigned long PeakRssKb = 0;
+ if (sscanf(VmHWM + 6, "%lu", &PeakRssKb) == 1)
+ {
+ OutMetrics.PeakWorkingSetSize = PeakRssKb * 1024;
+ }
+ }
+
+ const char* VmPeak = strstr(Buf, "VmPeak:");
+ if (VmPeak)
+ {
+ unsigned long PeakVmKb = 0;
+ if (sscanf(VmPeak + 7, "%lu", &PeakVmKb) == 1)
+ {
+ OutMetrics.PeakPagefileUsage = PeakVmKb * 1024;
+ }
+ }
+ }
+ }
+ }
+
+ OutMetrics.MemoryBytes = OutMetrics.WorkingSetSize;
+
+#elif ZEN_PLATFORM_MAC
+
+ const pid_t Pid = static_cast<pid_t>(Handle.Pid());
+
+ struct proc_taskinfo Info;
+ if (proc_pidinfo(Pid, PROC_PIDTASKINFO, 0, &Info, sizeof(Info)) > 0)
+ {
+ // pti_total_user and pti_total_system are in nanoseconds
+ OutMetrics.UserTimeMs = Info.pti_total_user / 1'000'000;
+ OutMetrics.KernelTimeMs = Info.pti_total_system / 1'000'000;
+
+ OutMetrics.WorkingSetSize = Info.pti_resident_size;
+ OutMetrics.PeakWorkingSetSize = Info.pti_resident_size; // macOS doesn't track peak RSS directly
+ OutMetrics.PagefileUsage = Info.pti_virtual_size;
+ OutMetrics.PeakPagefileUsage = Info.pti_virtual_size;
+ }
+
+ OutMetrics.MemoryBytes = OutMetrics.WorkingSetSize;
+
#endif
}
@@ -1639,6 +2224,24 @@ TEST_CASE("FindProcess")
}
}
+TEST_CASE("GetProcessMetrics")
+{
+ ProcessHandle Handle;
+ Handle.Initialize(GetCurrentProcessId());
+ REQUIRE(Handle.IsValid());
+
+ ProcessMetrics Metrics;
+ GetProcessMetrics(Handle, Metrics);
+
+ // The current process should have non-zero memory usage
+ CHECK(Metrics.WorkingSetSize > 0);
+ CHECK(Metrics.MemoryBytes > 0);
+ CHECK(Metrics.MemoryBytes == Metrics.WorkingSetSize);
+
+ // CPU time should be non-zero for a running test process
+ CHECK((Metrics.UserTimeMs + Metrics.KernelTimeMs) > 0);
+}
+
TEST_CASE("BuildArgV")
{
const char* Words[] = {"one", "two", "three", "four", "five"};