diff options
Diffstat (limited to 'src/zencore/process.cpp')
| -rw-r--r-- | src/zencore/process.cpp | 713 |
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"}; |