diff options
| author | Stefan Boberg <[email protected]> | 2023-05-02 10:01:47 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2023-05-02 10:01:47 +0200 |
| commit | 075d17f8ada47e990fe94606c3d21df409223465 (patch) | |
| tree | e50549b766a2f3c354798a54ff73404217b4c9af /src/zencore/thread.cpp | |
| parent | fix: bundle shouldn't append content zip to zen (diff) | |
| download | zen-075d17f8ada47e990fe94606c3d21df409223465.tar.xz zen-075d17f8ada47e990fe94606c3d21df409223465.zip | |
moved source directories into `/src` (#264)
* moved source directories into `/src`
* updated bundle.lua for new `src` path
* moved some docs, icon
* removed old test trees
Diffstat (limited to 'src/zencore/thread.cpp')
| -rw-r--r-- | src/zencore/thread.cpp | 1212 |
1 files changed, 1212 insertions, 0 deletions
diff --git a/src/zencore/thread.cpp b/src/zencore/thread.cpp new file mode 100644 index 000000000..1597a7dd9 --- /dev/null +++ b/src/zencore/thread.cpp @@ -0,0 +1,1212 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zencore/thread.h> + +#include <zencore/except.h> +#include <zencore/filesystem.h> +#include <zencore/scopeguard.h> +#include <zencore/string.h> +#include <zencore/testing.h> + +#if ZEN_PLATFORM_LINUX +# if !defined(_GNU_SOURCE) +# define _GNU_SOURCE // for semtimedop() +# endif +#endif + +#if ZEN_PLATFORM_WINDOWS +# include <shellapi.h> +# include <Shlobj.h> +# include <zencore/windows.h> +#else +# include <chrono> +# include <condition_variable> +# include <mutex> + +# include <fcntl.h> +# include <pthread.h> +# include <signal.h> +# include <sys/file.h> +# include <sys/sem.h> +# include <sys/stat.h> +# include <sys/syscall.h> +# include <sys/wait.h> +# include <time.h> +# include <unistd.h> +#endif + +#include <thread> + +ZEN_THIRD_PARTY_INCLUDES_START +#include <fmt/format.h> +ZEN_THIRD_PARTY_INCLUDES_END + +namespace zen { + +#if ZEN_PLATFORM_WINDOWS +// The information on how to set the thread name comes from +// a MSDN article: http://msdn2.microsoft.com/en-us/library/xcb2z8hs.aspx +const DWORD kVCThreadNameException = 0x406D1388; +typedef struct tagTHREADNAME_INFO +{ + DWORD dwType; // Must be 0x1000. + LPCSTR szName; // Pointer to name (in user addr space). + DWORD dwThreadID; // Thread ID (-1=caller thread). + DWORD dwFlags; // Reserved for future use, must be zero. +} THREADNAME_INFO; +// The SetThreadDescription API was brought in version 1607 of Windows 10. +typedef HRESULT(WINAPI* SetThreadDescription)(HANDLE hThread, PCWSTR lpThreadDescription); +// This function has try handling, so it is separated out of its caller. +void +SetNameInternal(DWORD thread_id, const char* name) +{ + THREADNAME_INFO info; + info.dwType = 0x1000; + info.szName = name; + info.dwThreadID = thread_id; + info.dwFlags = 0; + __try + { + RaiseException(kVCThreadNameException, 0, sizeof(info) / sizeof(DWORD), reinterpret_cast<DWORD_PTR*>(&info)); + } + __except (EXCEPTION_CONTINUE_EXECUTION) + { + } +} +#endif + +#if ZEN_PLATFORM_LINUX +const bool bNoZombieChildren = []() { + // When a child process exits it is put into a zombie state until the parent + // collects its result. This doesn't fit the Windows-like model that Zen uses + // where there is a less strict familial model and no zombification. Ignoring + // SIGCHLD siganals removes the need to call wait() on zombies. Another option + // would be for the child to call setsid() but that would detatch the child + // from the terminal. + struct sigaction Action = {}; + sigemptyset(&Action.sa_mask); + Action.sa_handler = SIG_IGN; + sigaction(SIGCHLD, &Action, nullptr); + return true; +}(); +#endif + +void +SetCurrentThreadName([[maybe_unused]] std::string_view ThreadName) +{ +#if ZEN_PLATFORM_WINDOWS + // The SetThreadDescription API works even if no debugger is attached. + static auto SetThreadDescriptionFunc = + reinterpret_cast<SetThreadDescription>(::GetProcAddress(::GetModuleHandle(L"Kernel32.dll"), "SetThreadDescription")); + + if (SetThreadDescriptionFunc) + { + SetThreadDescriptionFunc(::GetCurrentThread(), Utf8ToWide(ThreadName).c_str()); + } + // The debugger needs to be around to catch the name in the exception. If + // there isn't a debugger, we are just needlessly throwing an exception. + if (!::IsDebuggerPresent()) + return; + + std::string ThreadNameZ{ThreadName}; + SetNameInternal(GetCurrentThreadId(), ThreadNameZ.c_str()); +#else + std::string ThreadNameZ{ThreadName}; +# if ZEN_PLATFORM_MAC + pthread_setname_np(ThreadNameZ.c_str()); +# else + pthread_setname_np(pthread_self(), ThreadNameZ.c_str()); +# endif +#endif +} // namespace zen + +void +RwLock::AcquireShared() +{ + m_Mutex.lock_shared(); +} + +void +RwLock::ReleaseShared() +{ + m_Mutex.unlock_shared(); +} + +void +RwLock::AcquireExclusive() +{ + m_Mutex.lock(); +} + +void +RwLock::ReleaseExclusive() +{ + m_Mutex.unlock(); +} + +////////////////////////////////////////////////////////////////////////// + +#if !ZEN_PLATFORM_WINDOWS +struct EventInner +{ + std::mutex Mutex; + std::condition_variable CondVar; + bool volatile bSet = false; +}; +#endif // !ZEN_PLATFORM_WINDOWS + +Event::Event() +{ + bool bManualReset = true; + bool bInitialState = false; + +#if ZEN_PLATFORM_WINDOWS + m_EventHandle = CreateEvent(nullptr, bManualReset, bInitialState, nullptr); +#else + ZEN_UNUSED(bManualReset); + auto* Inner = new EventInner(); + Inner->bSet = bInitialState; + m_EventHandle = Inner; +#endif +} + +Event::~Event() +{ + Close(); +} + +void +Event::Set() +{ +#if ZEN_PLATFORM_WINDOWS + SetEvent(m_EventHandle); +#else + auto* Inner = (EventInner*)m_EventHandle; + { + std::unique_lock Lock(Inner->Mutex); + Inner->bSet = true; + } + Inner->CondVar.notify_all(); +#endif +} + +void +Event::Reset() +{ +#if ZEN_PLATFORM_WINDOWS + ResetEvent(m_EventHandle); +#else + auto* Inner = (EventInner*)m_EventHandle; + { + std::unique_lock Lock(Inner->Mutex); + Inner->bSet = false; + } +#endif +} + +void +Event::Close() +{ +#if ZEN_PLATFORM_WINDOWS + CloseHandle(m_EventHandle); +#else + auto* Inner = (EventInner*)m_EventHandle; + delete Inner; +#endif + m_EventHandle = nullptr; +} + +bool +Event::Wait(int TimeoutMs) +{ +#if ZEN_PLATFORM_WINDOWS + using namespace std::literals; + + const DWORD Timeout = (TimeoutMs < 0) ? INFINITE : TimeoutMs; + + DWORD Result = WaitForSingleObject(m_EventHandle, Timeout); + + if (Result == WAIT_FAILED) + { + zen::ThrowLastError("Event wait failed"sv); + } + + return (Result == WAIT_OBJECT_0); +#else + auto* Inner = (EventInner*)m_EventHandle; + + if (TimeoutMs >= 0) + { + std::unique_lock Lock(Inner->Mutex); + + if (Inner->bSet) + { + return true; + } + + return Inner->CondVar.wait_for(Lock, std::chrono::milliseconds(TimeoutMs), [&] { return Inner->bSet; }); + } + + std::unique_lock Lock(Inner->Mutex); + + if (!Inner->bSet) + { + Inner->CondVar.wait(Lock, [&] { return Inner->bSet; }); + } + + return true; +#endif +} + +////////////////////////////////////////////////////////////////////////// + +NamedEvent::NamedEvent(std::string_view EventName) +{ +#if ZEN_PLATFORM_WINDOWS + using namespace std::literals; + + ExtendableStringBuilder<64> Name; + Name << "Local\\"sv; + Name << EventName; + + m_EventHandle = CreateEventA(nullptr, true, false, Name.c_str()); +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + // Create a file to back the semaphore + ExtendableStringBuilder<64> EventPath; + EventPath << "/tmp/" << EventName; + + int Fd = open(EventPath.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0666); + if (Fd < 0) + { + ThrowLastError(fmt::format("Failed to create '{}' for named event", EventPath)); + } + fchmod(Fd, 0666); + + // Use the file path to generate an IPC key + key_t IpcKey = ftok(EventPath.c_str(), 1); + if (IpcKey < 0) + { + close(Fd); + ThrowLastError("Failed to create an SysV IPC key"); + } + + // Use the key to create/open the semaphore + int Sem = semget(IpcKey, 1, 0600 | IPC_CREAT); + if (Sem < 0) + { + close(Fd); + ThrowLastError("Failed creating an SysV semaphore"); + } + + // Atomically claim ownership of the semaphore's key. The owner initialises + // the semaphore to 1 so we can use the wait-for-zero op as that does not + // modify the semaphore's value on a successful wait. + int LockResult = flock(Fd, LOCK_EX | LOCK_NB); + if (LockResult == 0) + { + // This isn't thread safe really. Another thread could open the same + // semaphore and successfully wait on it in the period of time where + // this comment is but before the semaphore's initialised. + semctl(Sem, 0, SETVAL, 1); + } + + // Pack into the handle + static_assert(sizeof(Sem) + sizeof(Fd) <= sizeof(void*), "Semaphore packing assumptions not met"); + intptr_t Packed; + Packed = intptr_t(Sem) << 32; + Packed |= intptr_t(Fd) & 0xffff'ffff; + m_EventHandle = (void*)Packed; +#endif +} + +NamedEvent::~NamedEvent() +{ + Close(); +} + +void +NamedEvent::Close() +{ + if (m_EventHandle == nullptr) + { + return; + } + +#if ZEN_PLATFORM_WINDOWS + CloseHandle(m_EventHandle); +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + int Fd = int(intptr_t(m_EventHandle) & 0xffff'ffff); + + if (flock(Fd, LOCK_EX | LOCK_NB) == 0) + { + std::filesystem::path Name = PathFromHandle((void*)(intptr_t(Fd))); + unlink(Name.c_str()); + + flock(Fd, LOCK_UN | LOCK_NB); + close(Fd); + + int Sem = int(intptr_t(m_EventHandle) >> 32); + semctl(Sem, 0, IPC_RMID); + } +#endif + + m_EventHandle = nullptr; +} + +void +NamedEvent::Set() +{ +#if ZEN_PLATFORM_WINDOWS + SetEvent(m_EventHandle); +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + int Sem = int(intptr_t(m_EventHandle) >> 32); + semctl(Sem, 0, SETVAL, 0); +#endif +} + +bool +NamedEvent::Wait(int TimeoutMs) +{ +#if ZEN_PLATFORM_WINDOWS + const DWORD Timeout = (TimeoutMs < 0) ? INFINITE : TimeoutMs; + + DWORD Result = WaitForSingleObject(m_EventHandle, Timeout); + + if (Result == WAIT_FAILED) + { + using namespace std::literals; + zen::ThrowLastError("Event wait failed"sv); + } + + return (Result == WAIT_OBJECT_0); +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + int Sem = int(intptr_t(m_EventHandle) >> 32); + + int Result; + struct sembuf SemOp = {}; + + if (TimeoutMs < 0) + { + Result = semop(Sem, &SemOp, 1); + return Result == 0; + } + +# if defined(_GNU_SOURCE) + struct timespec TimeoutValue = { + .tv_sec = TimeoutMs >> 10, + .tv_nsec = (TimeoutMs & 0x3ff) << 20, + }; + Result = semtimedop(Sem, &SemOp, 1, &TimeoutValue); +# else + const int SleepTimeMs = 10; + SemOp.sem_flg = IPC_NOWAIT; + do + { + Result = semop(Sem, &SemOp, 1); + if (Result == 0 || errno != EAGAIN) + { + break; + } + + Sleep(SleepTimeMs); + TimeoutMs -= SleepTimeMs; + } while (TimeoutMs > 0); +# endif // _GNU_SOURCE + + return Result == 0; +#endif +} + +////////////////////////////////////////////////////////////////////////// + +NamedMutex::~NamedMutex() +{ +#if ZEN_PLATFORM_WINDOWS + if (m_MutexHandle) + { + CloseHandle(m_MutexHandle); + } +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + int Inner = int(intptr_t(m_MutexHandle)); + flock(Inner, LOCK_UN); + close(Inner); +#endif +} + +bool +NamedMutex::Create(std::string_view MutexName) +{ +#if ZEN_PLATFORM_WINDOWS + ZEN_ASSERT(m_MutexHandle == nullptr); + + using namespace std::literals; + + ExtendableStringBuilder<64> Name; + Name << "Global\\"sv; + Name << MutexName; + + m_MutexHandle = CreateMutexA(nullptr, /* InitialOwner */ TRUE, Name.c_str()); + + return !!m_MutexHandle; +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + ExtendableStringBuilder<64> Name; + Name << "/tmp/" << MutexName; + + int Inner = open(Name.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0666); + if (Inner < 0) + { + return false; + } + fchmod(Inner, 0666); + + if (flock(Inner, LOCK_EX) != 0) + { + close(Inner); + Inner = 0; + return false; + } + + m_MutexHandle = (void*)(intptr_t(Inner)); + return true; +#endif // ZEN_PLATFORM_WINDOWS +} + +bool +NamedMutex::Exists(std::string_view MutexName) +{ +#if ZEN_PLATFORM_WINDOWS + using namespace std::literals; + + ExtendableStringBuilder<64> Name; + Name << "Global\\"sv; + Name << MutexName; + + void* MutexHandle = OpenMutexA(SYNCHRONIZE, /* InheritHandle */ FALSE, Name.c_str()); + + if (MutexHandle == nullptr) + { + return false; + } + + CloseHandle(MutexHandle); + + return true; +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + ExtendableStringBuilder<64> Name; + Name << "/tmp/" << MutexName; + + bool bExists = false; + int Fd = open(Name.c_str(), O_RDWR | O_CLOEXEC); + if (Fd >= 0) + { + if (flock(Fd, LOCK_EX | LOCK_NB) == 0) + { + flock(Fd, LOCK_UN | LOCK_NB); + } + else + { + bExists = true; + } + close(Fd); + } + + return bExists; +#endif // ZEN_PLATFORM_WINDOWS +} + +////////////////////////////////////////////////////////////////////////// + +ProcessHandle::ProcessHandle() = default; + +#if ZEN_PLATFORM_WINDOWS +void +ProcessHandle::Initialize(void* ProcessHandle) +{ + ZEN_ASSERT(m_ProcessHandle == nullptr); + + if (ProcessHandle == INVALID_HANDLE_VALUE) + { + ProcessHandle = nullptr; + } + + // TODO: perform some debug verification here to verify it's a valid handle? + m_ProcessHandle = ProcessHandle; + m_Pid = GetProcessId(m_ProcessHandle); +} +#endif // ZEN_PLATFORM_WINDOWS + +ProcessHandle::~ProcessHandle() +{ + Reset(); +} + +void +ProcessHandle::Initialize(int Pid) +{ + ZEN_ASSERT(m_ProcessHandle == nullptr); + +#if ZEN_PLATFORM_WINDOWS + m_ProcessHandle = OpenProcess(PROCESS_QUERY_INFORMATION | SYNCHRONIZE, FALSE, Pid); +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + if (Pid > 0) + { + m_ProcessHandle = (void*)(intptr_t(Pid)); + } +#endif + + if (!m_ProcessHandle) + { + ThrowLastError(fmt::format("ProcessHandle::Initialize(pid: {}) failed", Pid)); + } + + m_Pid = Pid; +} + +bool +ProcessHandle::IsRunning() const +{ + bool bActive = false; + +#if ZEN_PLATFORM_WINDOWS + DWORD ExitCode = 0; + GetExitCodeProcess(m_ProcessHandle, &ExitCode); + bActive = (ExitCode == STILL_ACTIVE); +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + bActive = (kill(pid_t(m_Pid), 0) == 0); +#endif + + return bActive; +} + +bool +ProcessHandle::IsValid() const +{ + return (m_ProcessHandle != nullptr); +} + +void +ProcessHandle::Terminate(int ExitCode) +{ + if (!IsRunning()) + { + return; + } + + bool bSuccess = false; + +#if ZEN_PLATFORM_WINDOWS + TerminateProcess(m_ProcessHandle, ExitCode); + DWORD WaitResult = WaitForSingleObject(m_ProcessHandle, INFINITE); + bSuccess = (WaitResult != WAIT_OBJECT_0); +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + ZEN_UNUSED(ExitCode); + bSuccess = (kill(m_Pid, SIGKILL) == 0); +#endif + + if (!bSuccess) + { + // What might go wrong here, and what is meaningful to act on? + } +} + +void +ProcessHandle::Reset() +{ + if (IsValid()) + { +#if ZEN_PLATFORM_WINDOWS + CloseHandle(m_ProcessHandle); +#endif + m_ProcessHandle = nullptr; + m_Pid = 0; + } +} + +bool +ProcessHandle::Wait(int TimeoutMs) +{ + using namespace std::literals; + +#if ZEN_PLATFORM_WINDOWS + const DWORD Timeout = (TimeoutMs < 0) ? INFINITE : TimeoutMs; + + const DWORD WaitResult = WaitForSingleObject(m_ProcessHandle, Timeout); + + switch (WaitResult) + { + case WAIT_OBJECT_0: + return true; + + case WAIT_TIMEOUT: + return false; + + case WAIT_FAILED: + break; + } +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + const int SleepMs = 20; + timespec SleepTime = {0, SleepMs * 1000 * 1000}; + for (int i = 0;; i += SleepMs) + { +# if ZEN_PLATFORM_MAC + int WaitState = 0; + waitpid(m_Pid, &WaitState, WNOHANG | WCONTINUED | WUNTRACED); +# endif + + if (kill(m_Pid, 0) < 0) + { + if (zen::GetLastError() == ESRCH) + { + return true; + } + break; + } + + if (TimeoutMs >= 0 && i >= TimeoutMs) + { + return false; + } + + nanosleep(&SleepTime, nullptr); + } +#endif + + // What might go wrong here, and what is meaningful to act on? + ThrowLastError("Process::Wait failed"sv); +} + +////////////////////////////////////////////////////////////////////////// + +#if !ZEN_PLATFORM_WINDOWS || ZEN_WITH_TESTS +static void +BuildArgV(std::vector<char*>& Out, char* CommandLine) +{ + char* Cursor = CommandLine; + while (true) + { + // Skip leading whitespace + for (; *Cursor == ' '; ++Cursor) + ; + + // Check for nullp terminator + if (*Cursor == '\0') + { + break; + } + + Out.push_back(Cursor); + + // Extract word + int QuoteCount = 0; + do + { + QuoteCount += (*Cursor == '\"'); + if (*Cursor == ' ' && !(QuoteCount & 1)) + { + break; + } + ++Cursor; + } while (*Cursor != '\0'); + + if (*Cursor == '\0') + { + break; + } + + *Cursor = '\0'; + ++Cursor; + } +} +#endif // !WINDOWS || TESTS + +#if ZEN_PLATFORM_WINDOWS +static CreateProcResult +CreateProcNormal(const std::filesystem::path& Executable, std::string_view CommandLine, const CreateProcOptions& Options) +{ + PROCESS_INFORMATION ProcessInfo{}; + STARTUPINFO StartupInfo{.cb = sizeof(STARTUPINFO)}; + + const bool InheritHandles = false; + void* Environment = nullptr; + LPSECURITY_ATTRIBUTES ProcessAttributes = nullptr; + LPSECURITY_ATTRIBUTES ThreadAttributes = nullptr; + + DWORD CreationFlags = 0; + if (Options.Flags & CreateProcOptions::Flag_NewConsole) + { + CreationFlags |= CREATE_NEW_CONSOLE; + } + + const wchar_t* WorkingDir = nullptr; + if (Options.WorkingDirectory != nullptr) + { + WorkingDir = Options.WorkingDirectory->c_str(); + } + + ExtendableWideStringBuilder<256> CommandLineZ; + CommandLineZ << CommandLine; + + BOOL Success = CreateProcessW(Executable.c_str(), + CommandLineZ.Data(), + ProcessAttributes, + ThreadAttributes, + InheritHandles, + CreationFlags, + Environment, + WorkingDir, + &StartupInfo, + &ProcessInfo); + + if (!Success) + { + return nullptr; + } + + CloseHandle(ProcessInfo.hThread); + return ProcessInfo.hProcess; +} + +static CreateProcResult +CreateProcUnelevated(const std::filesystem::path& Executable, std::string_view CommandLine, const CreateProcOptions& Options) +{ + /* Launches a binary with the shell as its parent. The shell (such as + Explorer) should be an unelevated process. */ + + // No sense in using this route if we are not elevated in the first place + if (IsUserAnAdmin() == FALSE) + { + return CreateProcNormal(Executable, CommandLine, Options); + } + + // Get the users' shell process and open it for process creation + HWND ShellWnd = GetShellWindow(); + if (ShellWnd == nullptr) + { + return nullptr; + } + + DWORD ShellPid; + GetWindowThreadProcessId(ShellWnd, &ShellPid); + + HANDLE Process = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, ShellPid); + if (Process == nullptr) + { + return nullptr; + } + auto $0 = MakeGuard([&] { CloseHandle(Process); }); + + // Creating a process as a child of another process is done by setting a + // thread-attribute list on the startup info passed to CreateProcess() + SIZE_T AttrListSize; + InitializeProcThreadAttributeList(nullptr, 1, 0, &AttrListSize); + + auto AttrList = (PPROC_THREAD_ATTRIBUTE_LIST)malloc(AttrListSize); + auto $1 = MakeGuard([&] { free(AttrList); }); + + if (!InitializeProcThreadAttributeList(AttrList, 1, 0, &AttrListSize)) + { + return nullptr; + } + + BOOL bOk = + UpdateProcThreadAttribute(AttrList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, (HANDLE*)&Process, sizeof(Process), nullptr, nullptr); + if (!bOk) + { + return nullptr; + } + + // By this point we know we are an elevated process. It is not allowed to + // create a process as a child of another unelevated process that share our + // elevated console window if we have one. So we'll need to create a new one. + uint32_t CreateProcFlags = EXTENDED_STARTUPINFO_PRESENT; + if (GetConsoleWindow() != nullptr) + { + CreateProcFlags |= CREATE_NEW_CONSOLE; + } + else + { + CreateProcFlags |= DETACHED_PROCESS; + } + + // Everything is set up now so we can proceed and launch the process + STARTUPINFOEXW StartupInfo = { + .StartupInfo = {.cb = sizeof(STARTUPINFOEXW)}, + .lpAttributeList = AttrList, + }; + PROCESS_INFORMATION ProcessInfo = {}; + + if (Options.Flags & CreateProcOptions::Flag_NewConsole) + { + CreateProcFlags |= CREATE_NEW_CONSOLE; + } + + ExtendableWideStringBuilder<256> CommandLineZ; + CommandLineZ << CommandLine; + + bOk = CreateProcessW(Executable.c_str(), + CommandLineZ.Data(), + nullptr, + nullptr, + FALSE, + CreateProcFlags, + nullptr, + nullptr, + &StartupInfo.StartupInfo, + &ProcessInfo); + if (bOk == FALSE) + { + return nullptr; + } + + CloseHandle(ProcessInfo.hThread); + return ProcessInfo.hProcess; +} + +static CreateProcResult +CreateProcElevated(const std::filesystem::path& Executable, std::string_view CommandLine, const CreateProcOptions& Options) +{ + ExtendableWideStringBuilder<256> CommandLineZ; + CommandLineZ << CommandLine; + + SHELLEXECUTEINFO ShellExecuteInfo; + ZeroMemory(&ShellExecuteInfo, sizeof(ShellExecuteInfo)); + ShellExecuteInfo.cbSize = sizeof(ShellExecuteInfo); + ShellExecuteInfo.fMask = SEE_MASK_UNICODE | SEE_MASK_NOCLOSEPROCESS; + ShellExecuteInfo.lpFile = Executable.c_str(); + ShellExecuteInfo.lpVerb = TEXT("runas"); + ShellExecuteInfo.nShow = SW_SHOW; + ShellExecuteInfo.lpParameters = CommandLineZ.c_str(); + + if (Options.WorkingDirectory != nullptr) + { + ShellExecuteInfo.lpDirectory = Options.WorkingDirectory->c_str(); + } + + if (::ShellExecuteEx(&ShellExecuteInfo)) + { + return ShellExecuteInfo.hProcess; + } + + return nullptr; +} +#endif // ZEN_PLATFORM_WINDOWS + +CreateProcResult +CreateProc(const std::filesystem::path& Executable, std::string_view CommandLine, const CreateProcOptions& Options) +{ +#if ZEN_PLATFORM_WINDOWS + if (Options.Flags & CreateProcOptions::Flag_Unelevated) + { + return CreateProcUnelevated(Executable, CommandLine, Options); + } + + if (Options.Flags & CreateProcOptions::Flag_Elevated) + { + return CreateProcElevated(Executable, CommandLine, Options); + } + + return CreateProcNormal(Executable, CommandLine, Options); +#else + std::vector<char*> ArgV; + std::string CommandLineZ(CommandLine); + BuildArgV(ArgV, CommandLineZ.data()); + ArgV.push_back(nullptr); + + int ChildPid = fork(); + if (ChildPid < 0) + { + ThrowLastError("Failed to fork a new child process"); + } + else if (ChildPid == 0) + { + if (Options.WorkingDirectory != nullptr) + { + int Result = chdir(Options.WorkingDirectory->c_str()); + ZEN_UNUSED(Result); + } + + if (execv(Executable.c_str(), ArgV.data()) < 0) + { + ThrowLastError("Failed to exec() a new process image"); + } + } + + return ChildPid; +#endif +} + +////////////////////////////////////////////////////////////////////////// + +ProcessMonitor::ProcessMonitor() +{ +} + +ProcessMonitor::~ProcessMonitor() +{ + RwLock::ExclusiveLockScope _(m_Lock); + + for (HandleType& Proc : m_ProcessHandles) + { +#if ZEN_PLATFORM_WINDOWS + CloseHandle(Proc); +#endif + Proc = 0; + } +} + +bool +ProcessMonitor::IsRunning() +{ + RwLock::ExclusiveLockScope _(m_Lock); + + bool FoundOne = false; + + for (HandleType& Proc : m_ProcessHandles) + { + bool ProcIsActive; + +#if ZEN_PLATFORM_WINDOWS + DWORD ExitCode = 0; + GetExitCodeProcess(Proc, &ExitCode); + + ProcIsActive = (ExitCode == STILL_ACTIVE); + if (!ProcIsActive) + { + CloseHandle(Proc); + } +#else + int Pid = int(intptr_t(Proc)); + ProcIsActive = IsProcessRunning(Pid); +#endif + + if (!ProcIsActive) + { + Proc = 0; + } + + // Still alive + FoundOne |= ProcIsActive; + } + + std::erase_if(m_ProcessHandles, [](HandleType Handle) { return Handle == 0; }); + + return FoundOne; +} + +void +ProcessMonitor::AddPid(int Pid) +{ + HandleType ProcessHandle; + +#if ZEN_PLATFORM_WINDOWS + ProcessHandle = OpenProcess(PROCESS_QUERY_INFORMATION | SYNCHRONIZE, FALSE, Pid); +#else + ProcessHandle = HandleType(intptr_t(Pid)); +#endif + + if (ProcessHandle) + { + RwLock::ExclusiveLockScope _(m_Lock); + m_ProcessHandles.push_back(ProcessHandle); + } +} + +bool +ProcessMonitor::IsActive() const +{ + RwLock::SharedLockScope _(m_Lock); + return m_ProcessHandles.empty() == false; +} + +////////////////////////////////////////////////////////////////////////// + +bool +IsProcessRunning(int pid) +{ + // This function is arguably not super useful, a pid can be re-used + // by the OS so holding on to a pid and polling it over some time + // period will not necessarily tell you what you probably want to know. + +#if ZEN_PLATFORM_WINDOWS + HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + + if (!hProc) + { + DWORD Error = zen::GetLastError(); + + if (Error == ERROR_INVALID_PARAMETER) + { + return false; + } + + ThrowSystemError(Error, fmt::format("failed to open process with pid {}", pid)); + } + + CloseHandle(hProc); + + return true; +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + return (kill(pid_t(pid), 0) == 0); +#endif +} + +int +GetCurrentProcessId() +{ +#if ZEN_PLATFORM_WINDOWS + return ::GetCurrentProcessId(); +#else + return int(getpid()); +#endif +} + +int +GetCurrentThreadId() +{ +#if ZEN_PLATFORM_WINDOWS + return ::GetCurrentThreadId(); +#elif ZEN_PLATFORM_LINUX + return int(syscall(SYS_gettid)); +#elif ZEN_PLATFORM_MAC + return int(pthread_mach_thread_np(pthread_self())); +#endif +} + +void +Sleep(int ms) +{ +#if ZEN_PLATFORM_WINDOWS + ::Sleep(ms); +#else + usleep(ms * 1000U); +#endif +} + +////////////////////////////////////////////////////////////////////////// +// +// Testing related code follows... +// + +#if ZEN_WITH_TESTS + +void +thread_forcelink() +{ +} + +TEST_CASE("Thread") +{ + int Pid = GetCurrentProcessId(); + CHECK(Pid > 0); + CHECK(IsProcessRunning(Pid)); + + CHECK_FALSE(GetCurrentThreadId() == 0); +} + +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"}, + }; + + for (const auto& Case : Cases) + { + std::vector<char*> OutArgs; + StringBuilder<64> Mutable; + Mutable << Case.Input; + BuildArgV(OutArgs, Mutable.Data()); + + CHECK_EQ(OutArgs.size(), Case.WordCount); + + for (int i = 0, n = int(OutArgs.size()); i < n; ++i) + { + 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], '\"'); + } + } + } +} + +TEST_CASE("NamedEvent") +{ + std::string Name = "zencore_test_event"; + NamedEvent TestEvent(Name); + + // Timeout test + for (uint32_t i = 0; i < 8; ++i) + { + bool bEventSet = TestEvent.Wait(100); + CHECK(!bEventSet); + } + + // Thread check + std::thread Waiter = std::thread([Name]() { + NamedEvent ReadyEvent(Name + "_ready"); + ReadyEvent.Set(); + + NamedEvent TestEvent(Name); + TestEvent.Wait(1000); + }); + + NamedEvent ReadyEvent(Name + "_ready"); + ReadyEvent.Wait(); + + zen::Sleep(500); + TestEvent.Set(); + + Waiter.join(); + + // Manual reset property + for (uint32_t i = 0; i < 8; ++i) + { + bool bEventSet = TestEvent.Wait(100); + CHECK(bEventSet); + } +} + +TEST_CASE("NamedMutex") +{ + static const char* Name = "zen_test_mutex"; + + CHECK(!NamedMutex::Exists(Name)); + + { + NamedMutex TestMutex; + CHECK(TestMutex.Create(Name)); + CHECK(NamedMutex::Exists(Name)); + } + + CHECK(!NamedMutex::Exists(Name)); +} + +#endif // ZEN_WITH_TESTS + +} // namespace zen |