// Copyright Epic Games, Inc. All Rights Reserved. #include #include #include #include #include #include #include #include #if ZEN_PLATFORM_WINDOWS # include # include # include #else # include # include # include # include # include # include # include # include # include # include #endif ZEN_THIRD_PARTY_INCLUDES_START #include ZEN_THIRD_PARTY_INCLUDES_END namespace zen { #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 signals 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 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 (WIFEXITED(WaitState)) { m_ExitCode = WEXITSTATUS(WaitState); } if (kill(m_Pid, 0) < 0) { int32_t LastError = zen::GetLastError(); if (LastError == ESRCH) { return true; } ThrowSystemError(static_cast(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; #elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC return m_ExitCode; #else ZEN_NOT_IMPLEMENTED(); return 0; #endif } ////////////////////////////////////////////////////////////////////////// #if !ZEN_PLATFORM_WINDOWS || ZEN_WITH_TESTS static void BuildArgV(std::vector& 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)}; 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; if (!Options.StdoutFile.empty()) { SECURITY_ATTRIBUTES sa; sa.nLength = sizeof sa; sa.lpSecurityDescriptor = nullptr; sa.bInheritHandle = TRUE; StartupInfo.hStdInput = nullptr; StartupInfo.hStdOutput = CreateFileW(Options.StdoutFile.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, &sa, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); const BOOL Success = DuplicateHandle(GetCurrentProcess(), StartupInfo.hStdOutput, GetCurrentProcess(), &StartupInfo.hStdError, 0, TRUE, DUPLICATE_SAME_ACCESS); if (Success) { StartupInfo.dwFlags |= STARTF_USESTDHANDLES; InheritHandles = true; } else { CloseHandle(StartupInfo.hStdOutput); StartupInfo.hStdOutput = 0; } } BOOL Success = CreateProcessW(Executable.c_str(), CommandLineZ.Data(), ProcessAttributes, ThreadAttributes, InheritHandles, CreationFlags, Environment, WorkingDir, &StartupInfo, &ProcessInfo); if (StartupInfo.dwFlags & STARTF_USESTDHANDLES) { CloseHandle(StartupInfo.hStdError); CloseHandle(StartupInfo.hStdOutput); } 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; ExtendableWideStringBuilder<256> CurrentDirZ; LPCWSTR WorkingDirectoryPtr = nullptr; if (Options.WorkingDirectory) { CurrentDirZ << Options.WorkingDirectory->native(); WorkingDirectoryPtr = CurrentDirZ.c_str(); } bOk = CreateProcessW(Executable.c_str(), CommandLineZ.Data(), nullptr, nullptr, FALSE, CreateProcFlags, nullptr, WorkingDirectoryPtr, &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 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 GetProcessId(CreateProcResult ProcId) { #if ZEN_PLATFORM_WINDOWS return static_cast(::GetProcessId(ProcId)); #else return ProcId; #endif } #if ZEN_WITH_TESTS void process_forcelink() { } TEST_SUITE_BEGIN("core.process"); TEST_CASE("Process") { int Pid = GetCurrentProcessId(); CHECK(Pid > 0); CHECK(IsProcessRunning(Pid)); } 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 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_SUITE_END(/* core.process */); #endif } // namespace zen