diff options
Diffstat (limited to 'src/zencore/thread.cpp')
| -rw-r--r-- | src/zencore/thread.cpp | 680 |
1 files changed, 13 insertions, 667 deletions
diff --git a/src/zencore/thread.cpp b/src/zencore/thread.cpp index 6092895b0..149a0d781 100644 --- a/src/zencore/thread.cpp +++ b/src/zencore/thread.cpp @@ -8,6 +8,9 @@ #include <zencore/scopeguard.h> #include <zencore/string.h> #include <zencore/testing.h> +#include <zencore/trace.h> + +#include <thread> #if ZEN_PLATFORM_LINUX # if !defined(_GNU_SOURCE) @@ -15,15 +18,15 @@ # endif #endif -#if ZEN_PLATFORM_WINDOWS -# include <shellapi.h> -# include <Shlobj.h> -# include <zencore/windows.h> -#else +#if !ZEN_USE_WINDOWS_EVENTS # include <chrono> # include <condition_variable> # include <mutex> +#endif +#if ZEN_PLATFORM_WINDOWS +# include <zencore/windows.h> +#else # include <fcntl.h> # include <pthread.h> # include <signal.h> @@ -36,10 +39,6 @@ # include <unistd.h> #endif -#include <zencore/trace.h> - -#include <thread> - ZEN_THIRD_PARTY_INCLUDES_START #include <fmt/format.h> ZEN_THIRD_PARTY_INCLUDES_END @@ -78,22 +77,6 @@ SetNameInternal(DWORD thread_id, const char* name) } #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) { @@ -533,581 +516,6 @@ NamedMutex::Exists(std::string_view MutexName) #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 SleepedTimeMS = 0;; SleepedTimeMS += SleepMs) - { - int WaitState = 0; - waitpid(m_Pid, &WaitState, WNOHANG | WCONTINUED | WUNTRACED); - - if (kill(m_Pid, 0) < 0) - { - int32_t LastError = zen::GetLastError(); - if (LastError == ESRCH) - { - return true; - } - ThrowSystemError(static_cast<uint32_t>(LastError), "Process::Wait kill failed"sv); - } - - if (TimeoutMs >= 0 && SleepedTimeMS >= TimeoutMs) - { - return false; - } - - nanosleep(&SleepTime, nullptr); - } -#endif - - // What might go wrong here, and what is meaningful to act on? - ThrowLastError("Process::Wait failed"sv); -} - -int -ProcessHandle::WaitExitCode() -{ - Wait(-1); - -#if ZEN_PLATFORM_WINDOWS - DWORD ExitCode = 0; - GetExitCodeProcess(m_ProcessHandle, &ExitCode); - - ZEN_ASSERT(ExitCode != STILL_ACTIVE); - - return ExitCode; -#else - ZEN_NOT_IMPLEMENTED(); - - return 0; -#endif -} - -////////////////////////////////////////////////////////////////////////// - -#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)); - } - - bool bStillActive = true; - DWORD ExitCode = 0; - if (0 != GetExitCodeProcess(hProc, &ExitCode)) - { - bStillActive = ExitCode == STILL_ACTIVE; - } - else - { - ZEN_WARN("Unable to get exit code from handle for process '{}', treating the process as active", pid); - } - - CloseHandle(hProc); - - return bStillActive; -#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() { @@ -1120,16 +528,6 @@ GetCurrentThreadId() #endif } -int -GetProcessId(CreateProcResult ProcId) -{ -#if ZEN_PLATFORM_WINDOWS - return static_cast<int>(::GetProcessId(ProcId)); -#else - return ProcId; -#endif -} - void Sleep(int ms) { @@ -1152,65 +550,11 @@ thread_forcelink() { } -TEST_CASE("Thread") -{ - int Pid = GetCurrentProcessId(); - CHECK(Pid > 0); - CHECK(IsProcessRunning(Pid)); - - CHECK_FALSE(GetCurrentThreadId() == 0); -} +TEST_SUITE_BEGIN("core.thread"); -TEST_CASE("BuildArgV") +TEST_CASE("GetCurrentThreadId") { - 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], '\"'); - } - } - } + CHECK_FALSE(GetCurrentThreadId() == 0); } TEST_CASE("NamedEvent") @@ -1266,6 +610,8 @@ TEST_CASE("NamedMutex") CHECK(!NamedMutex::Exists(Name)); } +TEST_SUITE_END(); + #endif // ZEN_WITH_TESTS } // namespace zen |