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.cpp725
1 files changed, 615 insertions, 110 deletions
diff --git a/src/zencore/process.cpp b/src/zencore/process.cpp
index 9cbbfa56a..5d37c3715 100644
--- a/src/zencore/process.cpp
+++ b/src/zencore/process.cpp
@@ -28,6 +28,7 @@ ZEN_THIRD_PARTY_INCLUDES_START
# include <pthread.h>
# include <signal.h>
# include <sys/file.h>
+# include <sys/resource.h>
# include <sys/sem.h>
# include <sys/stat.h>
# include <sys/syscall.h>
@@ -37,7 +38,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,8 +138,68 @@ 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
@@ -209,6 +272,79 @@ CreateStdoutPipe(StdoutPipeHandles& OutPipe)
return true;
}
+StdinPipeHandles::~StdinPipeHandles()
+{
+ Close();
+}
+
+StdinPipeHandles::StdinPipeHandles(StdinPipeHandles&& Other) noexcept
+: ReadHandle(std::exchange(Other.ReadHandle, nullptr))
+, WriteHandle(std::exchange(Other.WriteHandle, nullptr))
+{
+}
+
+StdinPipeHandles&
+StdinPipeHandles::operator=(StdinPipeHandles&& Other) noexcept
+{
+ if (this != &Other)
+ {
+ Close();
+ ReadHandle = std::exchange(Other.ReadHandle, nullptr);
+ WriteHandle = std::exchange(Other.WriteHandle, nullptr);
+ }
+ return *this;
+}
+
+void
+StdinPipeHandles::CloseReadEnd()
+{
+ if (ReadHandle)
+ {
+ CloseHandle(ReadHandle);
+ ReadHandle = nullptr;
+ }
+}
+
+void
+StdinPipeHandles::CloseWriteEnd()
+{
+ if (WriteHandle)
+ {
+ CloseHandle(WriteHandle);
+ WriteHandle = nullptr;
+ }
+}
+
+void
+StdinPipeHandles::Close()
+{
+ CloseReadEnd();
+ CloseWriteEnd();
+}
+
+bool
+CreateStdinPipe(StdinPipeHandles& 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 write end should not be inherited by the child
+ SetHandleInformation(WriteHandle, HANDLE_FLAG_INHERIT, 0);
+
+ OutPipe.ReadHandle = ReadHandle;
+ OutPipe.WriteHandle = WriteHandle;
+ return true;
+}
+
#else
StdoutPipeHandles::~StdoutPipeHandles()
@@ -271,6 +407,72 @@ CreateStdoutPipe(StdoutPipeHandles& OutPipe)
return true;
}
+StdinPipeHandles::~StdinPipeHandles()
+{
+ Close();
+}
+
+StdinPipeHandles::StdinPipeHandles(StdinPipeHandles&& Other) noexcept
+: ReadFd(std::exchange(Other.ReadFd, -1))
+, WriteFd(std::exchange(Other.WriteFd, -1))
+{
+}
+
+StdinPipeHandles&
+StdinPipeHandles::operator=(StdinPipeHandles&& Other) noexcept
+{
+ if (this != &Other)
+ {
+ Close();
+ ReadFd = std::exchange(Other.ReadFd, -1);
+ WriteFd = std::exchange(Other.WriteFd, -1);
+ }
+ return *this;
+}
+
+void
+StdinPipeHandles::CloseReadEnd()
+{
+ if (ReadFd >= 0)
+ {
+ close(ReadFd);
+ ReadFd = -1;
+ }
+}
+
+void
+StdinPipeHandles::CloseWriteEnd()
+{
+ if (WriteFd >= 0)
+ {
+ close(WriteFd);
+ WriteFd = -1;
+ }
+}
+
+void
+StdinPipeHandles::Close()
+{
+ CloseReadEnd();
+ CloseWriteEnd();
+}
+
+bool
+CreateStdinPipe(StdinPipeHandles& OutPipe)
+{
+ int Fds[2];
+ if (pipe(Fds) != 0)
+ {
+ return false;
+ }
+ OutPipe.ReadFd = Fds[0];
+ OutPipe.WriteFd = Fds[1];
+
+ // Set close-on-exec on the write end so the child doesn't inherit it
+ fcntl(OutPipe.WriteFd, F_SETFD, FD_CLOEXEC);
+ return true;
+}
+
#endif
//////////////////////////////////////////////////////////////////////////
@@ -444,7 +646,7 @@ ProcessHandle::Kill()
std::error_code Ec;
if (!Wait(5000, Ec))
{
- // Graceful shutdown timed out — force-kill
+ // Graceful shutdown timed out - force-kill
kill(pid_t(m_Pid), SIGKILL);
Wait(1000, Ec);
}
@@ -652,47 +854,108 @@ ProcessHandle::WaitExitCode()
//////////////////////////////////////////////////////////////////////////
#if !ZEN_PLATFORM_WINDOWS || ZEN_WITH_TESTS
+// Splits an in-process command-line string into argv, in place. Double quotes
+// suppress space-splitting and are themselves stripped out (shell-style), so
+// `foo "a b" c` -> {"foo", "a b", "c"} and `--k="a b"` -> {"--k=a b"}.
+// Quotes do not have to wrap the whole token; pairs anywhere in a token are
+// removed. There is no escape mechanism — `\"` is not recognised.
static void
BuildArgV(std::vector<char*>& Out, char* CommandLine)
{
- char* Cursor = CommandLine;
+ char* Read = CommandLine;
while (true)
{
- // Skip leading whitespace
- for (; *Cursor == ' '; ++Cursor)
+ // Skip leading whitespace between tokens
+ for (; *Read == ' '; ++Read)
;
- // Check for nullp terminator
- if (*Cursor == '\0')
+ if (*Read == '\0')
{
break;
}
- Out.push_back(Cursor);
+ // Compact in place: Write trails Read, omitting quote chars
+ char* Write = Read;
+ Out.push_back(Write);
- // Extract word
- int QuoteCount = 0;
- do
+ bool InQuotes = false;
+ while (*Read != '\0')
{
- QuoteCount += (*Cursor == '\"');
- if (*Cursor == ' ' && !(QuoteCount & 1))
+ if (*Read == '\"')
+ {
+ InQuotes = !InQuotes;
+ ++Read;
+ continue;
+ }
+ if (*Read == ' ' && !InQuotes)
{
break;
}
- ++Cursor;
- } while (*Cursor != '\0');
+ *Write++ = *Read++;
+ }
- if (*Cursor == '\0')
+ const bool AtEnd = (*Read == '\0');
+ *Write = '\0';
+ if (AtEnd)
{
break;
}
-
- *Cursor = '\0';
- ++Cursor;
+ ++Read;
}
}
+
#endif // !WINDOWS || TESTS
+std::string
+GetRawCommandLine()
+{
+#if ZEN_PLATFORM_WINDOWS
+ LPWSTR Raw = ::GetCommandLineW();
+ if (Raw == nullptr)
+ {
+ return {};
+ }
+ return WideToUtf8(Raw);
+#else
+ return {};
+#endif
+}
+
+std::string
+BuildCommandLine(std::span<const std::string> Argv)
+{
+ constexpr AsciiSet QuoteChars = " \t\"";
+
+ std::string Result;
+ for (size_t I = 0; I < Argv.size(); ++I)
+ {
+ if (I > 0)
+ {
+ Result += ' ';
+ }
+
+ const std::string& Arg = Argv[I];
+ const bool NeedsQuotes = Arg.empty() || AsciiSet::HasAny(Arg.c_str(), QuoteChars);
+ if (!NeedsQuotes)
+ {
+ Result += Arg;
+ continue;
+ }
+
+ Result += '"';
+ for (char Ch : Arg)
+ {
+ if (Ch == '"')
+ {
+ Result += '\\';
+ }
+ Result += Ch;
+ }
+ Result += '"';
+ }
+ return Result;
+}
+
#if ZEN_PLATFORM_WINDOWS
static CreateProcResult
CreateProcNormal(const std::filesystem::path& Executable, std::string_view CommandLine, const CreateProcOptions& Options)
@@ -766,10 +1029,14 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
{
CreationFlags |= CREATE_NO_WINDOW;
}
- if (Options.Flags & CreateProcOptions::Flag_Windows_NewProcessGroup)
+ if (Options.Flags & CreateProcOptions::Flag_NewProcessGroup)
{
CreationFlags |= CREATE_NEW_PROCESS_GROUP;
}
+ if (Options.Flags & CreateProcOptions::Flag_BelowNormalPriority)
+ {
+ CreationFlags |= BELOW_NORMAL_PRIORITY_CLASS;
+ }
if (AssignToJob)
{
CreationFlags |= CREATE_SUSPENDED;
@@ -784,19 +1051,25 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
ExtendableWideStringBuilder<256> CommandLineZ;
CommandLineZ << CommandLine;
- bool DuplicatedStdErr = false;
+ bool DuplicatedStdErr = false;
+ bool CreatedStdOutFile = false;
+ bool UseStdHandles = false;
+
+ if (Options.StdinPipe != nullptr && Options.StdinPipe->ReadHandle != nullptr)
+ {
+ StartupInfo.hStdInput = (HANDLE)Options.StdinPipe->ReadHandle;
+ UseStdHandles = true;
+ }
if (Options.StdoutPipe != nullptr && Options.StdoutPipe->WriteHandle != nullptr)
{
- StartupInfo.hStdInput = nullptr;
StartupInfo.hStdOutput = (HANDLE)Options.StdoutPipe->WriteHandle;
+ UseStdHandles = true;
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
{
@@ -812,8 +1085,6 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
if (DupSuccess)
{
DuplicatedStdErr = true;
- StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
- InheritHandles = true;
}
}
}
@@ -824,7 +1095,6 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
sa.lpSecurityDescriptor = nullptr;
sa.bInheritHandle = TRUE;
- StartupInfo.hStdInput = nullptr;
StartupInfo.hStdOutput = CreateFileW(Options.StdoutFile.c_str(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
@@ -833,25 +1103,54 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
FILE_ATTRIBUTE_NORMAL,
nullptr);
- const BOOL Success = DuplicateHandle(GetCurrentProcess(),
- StartupInfo.hStdOutput,
- GetCurrentProcess(),
- &StartupInfo.hStdError,
- 0,
- TRUE,
- DUPLICATE_SAME_ACCESS);
+ if (StartupInfo.hStdOutput != INVALID_HANDLE_VALUE)
+ {
+ CreatedStdOutFile = true;
+ UseStdHandles = true;
+
+ const BOOL Success = DuplicateHandle(GetCurrentProcess(),
+ StartupInfo.hStdOutput,
+ GetCurrentProcess(),
+ &StartupInfo.hStdError,
+ 0,
+ TRUE,
+ DUPLICATE_SAME_ACCESS);
+
+ if (Success)
+ {
+ DuplicatedStdErr = true;
+ }
+ else
+ {
+ CloseHandle(StartupInfo.hStdOutput);
+ StartupInfo.hStdOutput = 0;
+ CreatedStdOutFile = false;
+ UseStdHandles = (Options.StdinPipe != nullptr && Options.StdinPipe->ReadHandle != nullptr);
+ }
+ }
+ }
- if (Success)
+ if (UseStdHandles)
+ {
+ // When STARTF_USESTDHANDLES is set, Windows requires all three handles to be
+ // specified. Fall back to the parent's current std handles for any that the
+ // caller didn't supply. This is best-effort: GetStdHandle may return handles
+ // that are not inheritable in a headless parent, in which case the child will
+ // see closed handles on those streams.
+ if (StartupInfo.hStdInput == nullptr)
{
- DuplicatedStdErr = true;
- StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
- InheritHandles = true;
+ StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
}
- else
+ if (StartupInfo.hStdOutput == nullptr)
{
- CloseHandle(StartupInfo.hStdOutput);
- StartupInfo.hStdOutput = 0;
+ StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
}
+ if (StartupInfo.hStdError == nullptr)
+ {
+ StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);
+ }
+ StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
+ InheritHandles = true;
}
BOOL Success = CreateProcessW(Executable.c_str(),
@@ -873,7 +1172,7 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
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)
+ if (CreatedStdOutFile)
{
CloseHandle(StartupInfo.hStdOutput);
}
@@ -980,6 +1279,10 @@ CreateProcUnelevated(const std::filesystem::path& Executable, std::string_view C
{
CreateProcFlags |= CREATE_NO_WINDOW;
}
+ if (Options.Flags & CreateProcOptions::Flag_BelowNormalPriority)
+ {
+ CreateProcFlags |= BELOW_NORMAL_PRIORITY_CLASS;
+ }
if (AssignToJob)
{
CreateProcFlags |= CREATE_SUSPENDED;
@@ -1070,23 +1373,37 @@ 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.StdinPipe != nullptr && Options.StdinPipe->ReadFd >= 0)
+ {
+ dup2(Options.StdinPipe->ReadFd, STDIN_FILENO);
+ close(Options.StdinPipe->ReadFd);
+ // WriteFd has FD_CLOEXEC so it's auto-closed on exec
}
if (Options.StdoutPipe != nullptr && Options.StdoutPipe->WriteFd >= 0)
@@ -1118,23 +1435,118 @@ CreateProc(const std::filesystem::path& Executable, std::string_view CommandLine
}
}
- if (Options.ProcessGroupId > 0)
+ if (Options.Flags & CreateProcOptions::Flag_NewProcessGroup)
+ {
+ setpgid(0, 0);
+ }
+ else if (Options.ProcessGroupId > 0)
{
setpgid(0, Options.ProcessGroupId);
}
- for (const auto& [Key, Value] : Options.Environment)
+ execve(Executable.c_str(), ArgV.data(), Envp.Data);
+ _exit(127);
+ }
+
+ if (Options.Flags & CreateProcOptions::Flag_BelowNormalPriority)
+ {
+ setpriority(PRIO_PROCESS, ChildPid, 5);
+ }
+
+ 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)
{
- setenv(Key.c_str(), Value.c_str(), 1);
+ ThrowSystemError(Err, "posix_spawn_file_actions_addchdir_np failed");
}
+ }
- if (execv(Executable.c_str(), ArgV.data()) < 0)
+ if (Options.StdinPipe != nullptr && Options.StdinPipe->ReadFd >= 0)
+ {
+ const int StdinReadFd = Options.StdinPipe->ReadFd;
+ ZEN_ASSERT(StdinReadFd > STDERR_FILENO);
+ posix_spawn_file_actions_adddup2(&FileActions, StdinReadFd, STDIN_FILENO);
+ posix_spawn_file_actions_addclose(&FileActions, StdinReadFd);
+ // WriteFd has FD_CLOEXEC so it's auto-closed on exec
+ }
+
+ 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)
{
- ThrowLastError("Failed to exec() a new process image");
+ 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);
}
- return ChildPid;
+ 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");
+ }
+
+ if (Options.Flags & CreateProcOptions::Flag_BelowNormalPriority)
+ {
+ setpriority(PRIO_PROCESS, ChildPid, 5);
+ }
+
+ return int(ChildPid);
#endif
}
@@ -1590,7 +2002,7 @@ GetProcessCommandLine(int Pid, std::error_code& OutEc)
++p; // skip null terminator of argv[0]
}
- // Build result: remaining entries joined by spaces (inter-arg nulls → spaces)
+ // Build result: remaining entries joined by spaces (inter-arg nulls -> spaces)
std::string Result;
Result.reserve(static_cast<size_t>(End - p));
for (const char* q = p; q < End; ++q)
@@ -1682,9 +2094,41 @@ GetProcessCommandLine(int Pid, std::error_code& OutEc)
#endif
}
+#if ZEN_PLATFORM_WINDOWS
+static const wchar_t*
+StripExtendedLengthPrefix(const wchar_t* S)
+{
+ // "\\?\C:\foo" -> "C:\foo"; "\\?\UNC\srv\share" -> "UNC\srv\share".
+ // UNC stripping is asymmetric vs a bare "\\srv\share" form, so this helper only normalizes
+ // the "\\?\" prefix itself. Comparing a "\\?\UNC\..." path to a bare "\\..." path is not
+ // expected for FindProcess inputs (zenserver installs are local paths).
+ if (S[0] == L'\\' && S[1] == L'\\' && S[2] == L'?' && S[3] == L'\\')
+ {
+ return S + 4;
+ }
+ return S;
+}
+#endif
+
+static bool
+IsSameNativePath(const std::filesystem::path& A, const std::filesystem::path& B)
+{
+#if ZEN_PLATFORM_WINDOWS
+ // Windows filesystem is case-insensitive; std::filesystem::path::operator== is not.
+ // CompareStringOrdinal is Microsoft's recommended API for filename/resource comparison:
+ // ordinal (no locale sensitivity) and case-insensitive when bIgnoreCase is TRUE.
+ // Strip "\\?\" extended-length prefix first so paths produced by MakeSafeAbsolutePath
+ // compare equal to paths from GetProcessExecutablePath which do not carry the prefix.
+ return CompareStringOrdinal(StripExtendedLengthPrefix(A.c_str()), -1, StripExtendedLengthPrefix(B.c_str()), -1, TRUE) == CSTR_EQUAL;
+#else
+ return A == B;
+#endif
+}
+
std::error_code
FindProcess(const std::filesystem::path& ExecutableImage, ProcessHandle& OutHandle, bool IncludeSelf)
{
+ const bool MatchFullPath = ExecutableImage.has_parent_path();
#if ZEN_PLATFORM_WINDOWS
HANDLE ProcessSnapshotHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (ProcessSnapshotHandle == INVALID_HANDLE_VALUE)
@@ -1701,29 +2145,41 @@ FindProcess(const std::filesystem::path& ExecutableImage, ProcessHandle& OutHand
{
do
{
- if ((IncludeSelf || (Entry.th32ProcessID != ThisProcessId)) && (ExecutableImage.filename() == Entry.szExeFile))
+ if ((IncludeSelf || (Entry.th32ProcessID != ThisProcessId)) && IsSameNativePath(ExecutableImage.filename(), Entry.szExeFile))
{
- std::error_code Ec;
- std::filesystem::path EntryPath = GetProcessExecutablePath(Entry.th32ProcessID, Ec);
- if (!Ec)
+ HANDLE Handle = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE | PROCESS_QUERY_INFORMATION, FALSE, Entry.th32ProcessID);
+ if (Handle == NULL)
{
- if (EntryPath == ExecutableImage)
+ // Skip processes we can't open (access denied, exited between snapshot and open, etc.)
+ continue;
+ }
+ // Close on all exits from this iteration unless ownership transfers to OutHandle (Handle set to NULL).
+ auto HandleGuard = MakeGuard([&]() {
+ if (Handle != NULL)
+ {
+ CloseHandle(Handle);
+ Handle = NULL;
+ }
+ });
+ DWORD ExitCode = 0;
+ bool Match = false;
+ if (GetExitCodeProcess(Handle, &ExitCode) && ExitCode == STILL_ACTIVE)
+ {
+ // Re-verify executable path post-open so PID reuse between snapshot and open is caught.
+ std::error_code Ec;
+ std::filesystem::path EntryPath = GetProcessExecutablePath(Entry.th32ProcessID, Ec);
+ if (!Ec)
{
- HANDLE Handle =
- OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE | PROCESS_QUERY_INFORMATION, FALSE, Entry.th32ProcessID);
- if (Handle == NULL)
- {
- return MakeErrorCodeFromLastError();
- }
- DWORD ExitCode = 0;
- GetExitCodeProcess(Handle, &ExitCode);
- if (ExitCode == STILL_ACTIVE)
- {
- OutHandle.Initialize((void*)Handle);
- return {};
- }
+ Match = MatchFullPath ? IsSameNativePath(EntryPath, ExecutableImage)
+ : IsSameNativePath(EntryPath.filename(), ExecutableImage.filename());
}
}
+ if (Match)
+ {
+ OutHandle.Initialize((void*)Handle);
+ Handle = NULL;
+ return {};
+ }
}
} while (::Process32Next(ProcessSnapshotHandle, (LPPROCESSENTRY32)&Entry));
return {};
@@ -1773,7 +2229,9 @@ FindProcess(const std::filesystem::path& ExecutableImage, ProcessHandle& OutHand
std::filesystem::path EntryPath = GetProcessExecutablePath(Pid, Ec);
if (!Ec)
{
- if (EntryPath == ExecutableImage)
+ const bool Match = MatchFullPath ? IsSameNativePath(EntryPath, ExecutableImage)
+ : IsSameNativePath(EntryPath.filename(), ExecutableImage.filename());
+ if (Match)
{
if (Processes[ProcIndex].kp_proc.p_stat != SZOMB)
{
@@ -1811,7 +2269,9 @@ FindProcess(const std::filesystem::path& ExecutableImage, ProcessHandle& OutHand
std::filesystem::path EntryPath = GetProcessExecutablePath((int)Pid, Ec);
if (!Ec)
{
- if (EntryPath == ExecutableImage)
+ const bool Match = MatchFullPath ? IsSameNativePath(EntryPath, ExecutableImage)
+ : IsSameNativePath(EntryPath.filename(), ExecutableImage.filename());
+ if (Match)
{
char Status = GetPidStatus(Pid, Ec);
if (!Ec)
@@ -1928,7 +2388,7 @@ GetProcessMetrics(const ProcessHandle& Handle, ProcessMetrics& OutMetrics)
{
Buf[Len] = '\0';
- // Skip past "pid (name) " — find last ')' to handle names containing spaces or parens
+ // Skip past "pid (name) " - find last ')' to handle names containing spaces or parens
const char* P = strrchr(Buf, ')');
if (P)
{
@@ -2076,6 +2536,54 @@ TEST_CASE("FindProcess")
CHECK(!Ec);
CHECK(!Process.IsValid());
}
+ {
+ ProcessHandle Process;
+ std::filesystem::path BareName = GetRunningExecutablePath().filename();
+ std::error_code Ec = FindProcess(BareName, Process, /*IncludeSelf*/ true);
+ CHECK(!Ec);
+ CHECK(Process.IsValid());
+ }
+ {
+ ProcessHandle Process;
+ std::error_code Ec = FindProcess("this-executable-definitely-does-not-exist.xyz", Process, /*IncludeSelf*/ true);
+ CHECK(!Ec);
+ CHECK(!Process.IsValid());
+ }
+ {
+ // Correct filename but wrong directory must not match when a parent path is supplied.
+ std::filesystem::path WrongPath = std::filesystem::path("nonexistent-dir-7f3a9c1e") / GetRunningExecutablePath().filename();
+ ProcessHandle Process;
+ std::error_code Ec = FindProcess(WrongPath, Process, /*IncludeSelf*/ true);
+ CHECK(!Ec);
+ CHECK(!Process.IsValid());
+ }
+# if ZEN_PLATFORM_WINDOWS
+ {
+ // On Windows, filename match is case-insensitive (filesystem is case-insensitive).
+ std::filesystem::path::string_type Upper = GetRunningExecutablePath().filename().native();
+ for (auto& Ch : Upper)
+ {
+ Ch = towupper(Ch);
+ }
+ ProcessHandle Process;
+ std::error_code Ec = FindProcess(std::filesystem::path(Upper), Process, /*IncludeSelf*/ true);
+ CHECK(!Ec);
+ CHECK(Process.IsValid());
+ }
+ {
+ // "\\?\"-prefixed absolute path must still match a running process whose reported path is unprefixed.
+ std::filesystem::path AbsPath = MakeSafeAbsolutePath(GetRunningExecutablePath());
+ std::filesystem::path::string_type Prefixed = AbsPath.native();
+ if (Prefixed.rfind(L"\\\\?\\", 0) != 0)
+ {
+ Prefixed.insert(0, L"\\\\?\\");
+ }
+ ProcessHandle Process;
+ std::error_code Ec = FindProcess(std::filesystem::path(Prefixed), Process, /*IncludeSelf*/ true);
+ CHECK(!Ec);
+ CHECK(Process.IsValid());
+ }
+# endif
}
TEST_CASE("GetProcessMetrics")
@@ -2098,52 +2606,49 @@ TEST_CASE("GetProcessMetrics")
TEST_CASE("BuildArgV")
{
- const char* Words[] = {"one", "two", "three", "four", "five"};
- struct
- {
- int WordCount;
- const char* Input;
- } Cases[] = {
- {0, ""},
- {0, " "},
- {1, "one"},
- {1, " one"},
- {1, "one "},
- {2, "one two"},
- {2, " one two"},
- {2, "one two "},
- {2, " one two"},
- {2, "one two "},
- {2, "one two "},
- {3, "one two three"},
- {3, "\"one\" two \"three\""},
- {5, "one two three four five"},
+ struct Case
+ {
+ std::initializer_list<const char*> Expected;
+ const char* Input;
+ };
+ const Case Cases[] = {
+ {{}, ""},
+ {{}, " "},
+ {{"one"}, "one"},
+ {{"one"}, " one"},
+ {{"one"}, "one "},
+ {{"one", "two"}, "one two"},
+ {{"one", "two"}, " one two"},
+ {{"one", "two"}, "one two "},
+ {{"one", "two", "three"}, "one two three"},
+ {{"one", "two", "three", "four", "five"}, "one two three four five"},
+
+ // Quotes are stripped (shell-style) and suppress space-splitting
+ {{"one", "two", "three"}, "\"one\" two \"three\""},
+ {{"hello world"}, "\"hello world\""},
+ {{"--key=hello world"}, "--key=\"hello world\""},
+ {{"--key=hello world"}, "\"--key=hello world\""},
+ {{"a b", "c d"}, "\"a b\" \"c d\""},
+ {{"abc"}, "a\"b\"c"},
+ {{""}, "\"\""},
+ {{"foo", "bar baz", "qux"}, "foo \"bar baz\" qux"},
};
for (const auto& Case : Cases)
{
std::vector<char*> OutArgs;
- StringBuilder<64> Mutable;
+ StringBuilder<128> Mutable;
Mutable << Case.Input;
BuildArgV(OutArgs, Mutable.Data());
- CHECK_EQ(OutArgs.size(), Case.WordCount);
+ REQUIRE_EQ(OutArgs.size(), Case.Expected.size());
- for (int i = 0, n = int(OutArgs.size()); i < n; ++i)
+ size_t i = 0;
+ for (const char* Truth : Case.Expected)
{
- const char* Truth = Words[i];
- size_t TruthLen = strlen(Truth);
-
- const char* Candidate = OutArgs[i];
- bool bQuoted = (Candidate[0] == '\"');
- Candidate += bQuoted;
-
- CHECK(strncmp(Truth, Candidate, TruthLen) == 0);
-
- if (bQuoted)
- {
- CHECK_EQ(Candidate[TruthLen], '\"');
- }
+ CHECK_MESSAGE(std::string_view(OutArgs[i]) == std::string_view(Truth),
+ fmt::format("input='{}' arg[{}]='{}' expected='{}'", Case.Input, i, OutArgs[i], Truth));
+ ++i;
}
}
}