diff options
| author | Stefan Boberg <[email protected]> | 2026-01-19 13:14:42 +0100 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-01-19 13:14:42 +0100 |
| commit | 5081a33dcd19c5d6f9d9cdae9f8745ba786374d4 (patch) | |
| tree | 6733e6f69137afc690a8c6dcdf618e69ce8029c5 /src | |
| parent | Merge pull request #701 from ue-foundation/lm/mac-daemon-mode (diff) | |
| download | zen-5081a33dcd19c5d6f9d9cdae9f8745ba786374d4.tar.xz zen-5081a33dcd19c5d6f9d9cdae9f8745ba786374d4.zip | |
consul package and basic client added (#716)
* this adds a consul package which can be used to fetch a consul binary
* it also adds a `ConsulProcess` helper which can be used to spawn and manage a consul service instance
* zencore dependencies brought across:
- `except_fmt.h` for easer generation of formatted exception messages
- `process.h/cpp` changes (adds `Kill` operation and process group support on Windows)
- `string.h` changes to allow generic use of `WideToUtf8()`
Diffstat (limited to 'src')
| -rw-r--r-- | src/zencore/filesystem.cpp | 2 | ||||
| -rw-r--r-- | src/zencore/include/zencore/except_fmt.h | 36 | ||||
| -rw-r--r-- | src/zencore/include/zencore/filesystem.h | 2 | ||||
| -rw-r--r-- | src/zencore/include/zencore/process.h | 61 | ||||
| -rw-r--r-- | src/zencore/include/zencore/string.h | 16 | ||||
| -rw-r--r-- | src/zencore/process.cpp | 142 | ||||
| -rw-r--r-- | src/zenutil/consul/consul.cpp | 140 | ||||
| -rw-r--r-- | src/zenutil/include/zenutil/consul.h | 47 |
8 files changed, 426 insertions, 20 deletions
diff --git a/src/zencore/filesystem.cpp b/src/zencore/filesystem.cpp index 4da412c17..c34dba23c 100644 --- a/src/zencore/filesystem.cpp +++ b/src/zencore/filesystem.cpp @@ -1592,7 +1592,7 @@ ReadStdIn() } FileContents -ReadFile(std::filesystem::path Path) +ReadFile(const std::filesystem::path& Path) { uint64_t FileSizeBytes; void* Handle; diff --git a/src/zencore/include/zencore/except_fmt.h b/src/zencore/include/zencore/except_fmt.h new file mode 100644 index 000000000..095a78da7 --- /dev/null +++ b/src/zencore/include/zencore/except_fmt.h @@ -0,0 +1,36 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include <fmt/args.h> +#include <fmt/format.h> +#include "except.h" + +namespace zen { + +/** + * Exception helper class to make formatted exception messages slightly easier by + * avoiding the need to explicitly call fmt::format(), making for less visual noise. + * + * Usage: + * + * throw zen::runtime_error("Failed to open file '{}'", FileName); + * + */ +template<typename BaseT> +class format_exception : public BaseT +{ +public: + format_exception(std::string_view Message) : BaseT(std::string(Message)) {} + + template<typename... T> + format_exception(fmt::format_string<T...> fmt, T&&... args) : BaseT(fmt::format(fmt, std::forward<T>(args)...)) + { + } +}; + +using runtime_error = format_exception<std::runtime_error>; +using invalid_argument = format_exception<std::invalid_argument>; +using out_of_range = format_exception<std::out_of_range>; + +} // namespace zen diff --git a/src/zencore/include/zencore/filesystem.h b/src/zencore/include/zencore/filesystem.h index 938a05b59..53cb550c1 100644 --- a/src/zencore/include/zencore/filesystem.h +++ b/src/zencore/include/zencore/filesystem.h @@ -159,7 +159,7 @@ ZENCORE_API FileContents ReadStdIn(); IoBuffer referencing the file contents so that it may be read at a later time. This is leveraged to allow sending of data straight from disk cache and other optimizations. */ -ZENCORE_API FileContents ReadFile(std::filesystem::path Path); +ZENCORE_API FileContents ReadFile(const std::filesystem::path& Path); ZENCORE_API bool ScanFile(std::filesystem::path Path, uint64_t ChunkSize, std::function<void(const void* Data, size_t Size)>&& ProcessFunc); ZENCORE_API void WriteFile(std::filesystem::path Path, const IoBuffer* const* Data, size_t BufferCount); diff --git a/src/zencore/include/zencore/process.h b/src/zencore/include/zencore/process.h index 04b79a1e0..d8d2a2b6b 100644 --- a/src/zencore/include/zencore/process.h +++ b/src/zencore/include/zencore/process.h @@ -14,26 +14,27 @@ namespace zen { class ProcessHandle { public: - ZENCORE_API ProcessHandle(); + ProcessHandle(); ProcessHandle(const ProcessHandle&) = delete; ProcessHandle& operator=(const ProcessHandle&) = delete; - ZENCORE_API ~ProcessHandle(); - - ZENCORE_API void Initialize(int Pid); - ZENCORE_API void Initialize(int Pid, std::error_code& OutEc); - ZENCORE_API void Initialize(void* ProcessHandle); /// Initialize with an existing handle - takes ownership of the handle - ZENCORE_API [[nodiscard]] bool IsRunning() const; - ZENCORE_API [[nodiscard]] bool IsValid() const; - ZENCORE_API bool Wait(int TimeoutMs = -1); - ZENCORE_API bool Wait(int TimeoutMs, std::error_code& OutEc); - ZENCORE_API int WaitExitCode(); - ZENCORE_API int GetExitCode(); - ZENCORE_API bool Terminate(int ExitCode); - ZENCORE_API void Reset(); - [[nodiscard]] inline int Pid() const { return m_Pid; } - [[nodiscard]] inline void* Handle() const { return m_ProcessHandle; } + ~ProcessHandle(); + + void Initialize(int Pid); + void Initialize(int Pid, std::error_code& OutEc); + void Initialize(void* ProcessHandle); /// Initialize with an existing handle - takes ownership of the handle + [[nodiscard]] bool IsRunning() const; + [[nodiscard]] bool IsValid() const; + bool Wait(int TimeoutMs = -1); + bool Wait(int TimeoutMs, std::error_code& OutEc); + int WaitExitCode(); + int GetExitCode(); + bool Kill(); + bool Terminate(int ExitCode); + void Reset(); + [[nodiscard]] inline int Pid() const { return m_Pid; } + [[nodiscard]] inline void* Handle() const { return m_ProcessHandle; } private: void* m_ProcessHandle = nullptr; @@ -53,6 +54,10 @@ struct CreateProcOptions Flag_Elevated = 1 << 1, Flag_Unelevated = 1 << 2, Flag_NoConsole = 1 << 3, + // This flag creates the new process in a new process group. This is relevant only on Windows, and + // allows sending ctrl-break events to the new process group without also sending it to the current + // process. + Flag_Windows_NewProcessGroup = 1 << 4, }; const std::filesystem::path* WorkingDirectory = nullptr; @@ -102,10 +107,34 @@ int GetProcessId(CreateProcResult ProcId); std::filesystem::path GetProcessExecutablePath(int Pid, std::error_code& OutEc); std::error_code FindProcess(const std::filesystem::path& ExecutableImage, ProcessHandle& OutHandle, bool IncludeSelf = true); +/** Wait for all threads in the current process to exit (except the calling thread) + * + * This is only implemented on Windows currently. The use-case for this is to try + * and ensure that all threads have exited before exiting main() in order to + * avoid some issues which can occur if threads are still running during process + * shutdown especially when the CRT is statically linked into the executable. + * + * This is a best-effort function and may return before all threads have exited. + */ +void WaitForThreads(uint64_t WaitTimeMs); + #if ZEN_PLATFORM_LINUX void IgnoreChildSignals(); #endif +struct ProcessMetrics +{ + uint64_t MemoryBytes = 0; + uint64_t KernelTimeMs = 0; + uint64_t UserTimeMs = 0; + uint64_t WorkingSetSize = 0; + uint64_t PeakWorkingSetSize = 0; + uint64_t PagefileUsage = 0; + uint64_t PeakPagefileUsage = 0; +}; + +void GetProcessMetrics(ProcessHandle& Handle, ProcessMetrics& OutMetrics); + void process_forcelink(); // internal } // namespace zen diff --git a/src/zencore/include/zencore/string.h b/src/zencore/include/zencore/string.h index 4379f2f80..5abdaf413 100644 --- a/src/zencore/include/zencore/string.h +++ b/src/zencore/include/zencore/string.h @@ -535,6 +535,22 @@ std::string WideToUtf8(const wchar_t* wstr); void WideToUtf8(const std::wstring_view& wstr, StringBuilderBase& out); std::string WideToUtf8(const std::wstring_view Wstr); +// This is a no-op helper to make it easier to write code that works with both +// narrow and wide strings +inline std::string +WideToUtf8(const char* str8) +{ + return str8; +} + +inline std::string +WideToUtf8(const std::string_view str8) +{ + return std::string(str8); +} + +////////////////////////////////////////////////////////////////////////// + inline uint8_t Char2Nibble(char c) { diff --git a/src/zencore/process.cpp b/src/zencore/process.cpp index 0b25d14f4..56849a10d 100644 --- a/src/zencore/process.cpp +++ b/src/zencore/process.cpp @@ -8,14 +8,19 @@ #include <zencore/scopeguard.h> #include <zencore/string.h> #include <zencore/testing.h> +#include <zencore/timer.h> #include <thread> +ZEN_THIRD_PARTY_INCLUDES_START + #if ZEN_PLATFORM_WINDOWS +# include <zencore/windows.h> + +# include <Psapi.h> # include <shellapi.h> # include <Shlobj.h> # include <TlHelp32.h> -# include <zencore/windows.h> #else # include <fcntl.h> # include <pthread.h> @@ -35,8 +40,8 @@ # include <sys/sysctl.h> #endif -ZEN_THIRD_PARTY_INCLUDES_START #include <fmt/format.h> + ZEN_THIRD_PARTY_INCLUDES_END namespace zen { @@ -215,6 +220,51 @@ ProcessHandle::IsValid() const } bool +ProcessHandle::Kill() +{ + if (!IsRunning()) + { + return true; + } + +#if ZEN_PLATFORM_WINDOWS + SetConsoleCtrlHandler(nullptr, TRUE); // Prevent this process from terminating itself + auto _ = MakeGuard([] { SetConsoleCtrlHandler(nullptr, FALSE); }); + + // Try graceful shutdown first + if (GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, m_Pid)) + { + // Wait briefly for graceful shutdown + if (WaitForSingleObject(m_ProcessHandle, 5000) == WAIT_OBJECT_0) + { + Reset(); + return true; + } + } + + // Fall back to forceful termination if graceful shutdown failed + if (!TerminateProcess(m_ProcessHandle, 0)) + { + return false; + } +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + int Res = kill(pid_t(m_Pid), SIGTERM); + if (Res != 0) + { + int err = errno; + if (err != ESRCH) + { + return false; + } + } +#endif + + Reset(); + + return true; +} + +bool ProcessHandle::Terminate(int ExitCode) { if (!IsRunning()) @@ -449,6 +499,10 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma { CreationFlags |= CREATE_NO_WINDOW; } + if (Options.Flags & CreateProcOptions::Flag_Windows_NewProcessGroup) + { + CreationFlags |= CREATE_NEW_PROCESS_GROUP; + } const wchar_t* WorkingDir = nullptr; if (Options.WorkingDirectory != nullptr) @@ -1082,6 +1136,90 @@ FindProcess(const std::filesystem::path& ExecutableImage, ProcessHandle& OutHand #endif // ZEN_PLATFORM_LINUX } +void +WaitForThreads(uint64_t WaitTimeMs) +{ +#if ZEN_PLATFORM_WINDOWS + auto ThreadSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (ThreadSnapshot == INVALID_HANDLE_VALUE) + { + return; + } + auto _ = MakeGuard([ThreadSnapshot] { CloseHandle(ThreadSnapshot); }); + + const DWORD CurrentProcessId = ::GetCurrentProcessId(); + const DWORD CurrentThreadId = ::GetCurrentThreadId(); + + Stopwatch Timer; + + do + { + THREADENTRY32 ThreadEntry; + ThreadEntry.dwSize = sizeof(THREADENTRY32); + + uint32_t ThreadCount = 0; + if (Thread32First(ThreadSnapshot, &ThreadEntry)) + { + do + { + if (ThreadEntry.th32OwnerProcessID == CurrentProcessId && ThreadEntry.th32ThreadID != CurrentThreadId) + { + ThreadCount++; + } + } while (Thread32Next(ThreadSnapshot, &ThreadEntry)); + } + + if (ThreadCount <= 1) + { + break; + } + + const uint64_t SleepMs = 10; + Sleep(SleepMs); + } while (Timer.GetElapsedTimeMs() < WaitTimeMs); +#else + ZEN_UNUSED(WaitTimeMs); +#endif +} + +void +GetProcessMetrics(ProcessHandle& Handle, ProcessMetrics& OutMetrics) +{ +#if ZEN_PLATFORM_WINDOWS + FILETIME CreationTime; + FILETIME ExitTime; + FILETIME KernelTime; + FILETIME UserTime; + + if (GetProcessTimes(Handle.Handle(), &CreationTime, &ExitTime, &KernelTime, &UserTime)) + { + ULARGE_INTEGER KTime; + KTime.LowPart = KernelTime.dwLowDateTime; + KTime.HighPart = KernelTime.dwHighDateTime; + + ULARGE_INTEGER UTime; + UTime.LowPart = UserTime.dwLowDateTime; + UTime.HighPart = UserTime.dwHighDateTime; + + OutMetrics.KernelTimeMs = KTime.QuadPart / 10000; + OutMetrics.UserTimeMs = UTime.QuadPart / 10000; + } + + PROCESS_MEMORY_COUNTERS MemCounters; + if (GetProcessMemoryInfo(Handle.Handle(), &MemCounters, sizeof(MemCounters))) + { + OutMetrics.WorkingSetSize = MemCounters.WorkingSetSize; + OutMetrics.PeakWorkingSetSize = MemCounters.PeakWorkingSetSize; + OutMetrics.PagefileUsage = MemCounters.PagefileUsage; + OutMetrics.PeakPagefileUsage = MemCounters.PeakPagefileUsage; + } +#else + // TODO: implement for Linux and Mac + ZEN_UNUSED(Handle); + ZEN_UNUSED(OutMetrics); +#endif +} + #if ZEN_WITH_TESTS void diff --git a/src/zenutil/consul/consul.cpp b/src/zenutil/consul/consul.cpp new file mode 100644 index 000000000..6ddebf97a --- /dev/null +++ b/src/zenutil/consul/consul.cpp @@ -0,0 +1,140 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zenutil/consul.h> + +#include <zencore/except_fmt.h> +#include <zencore/fmtutils.h> +#include <zencore/logging.h> +#include <zencore/process.h> +#include <zencore/string.h> +#include <zencore/timer.h> + +#include <fmt/format.h> + +namespace zen::consul { + +////////////////////////////////////////////////////////////////////////// + +struct ConsulProcess::Impl +{ + Impl(std::string_view BaseUri) : m_HttpClient(BaseUri) {} + ~Impl() = default; + + void SpawnConsulAgent() + { + if (m_ProcessHandle.IsValid()) + { + return; + } + + CreateProcOptions Options; + Options.Flags |= CreateProcOptions::Flag_Windows_NewProcessGroup; + + CreateProcResult Result = CreateProc("consul" ZEN_EXE_SUFFIX_LITERAL, "consul" ZEN_EXE_SUFFIX_LITERAL " agent -dev", Options); + + if (Result) + { + m_ProcessHandle.Initialize(Result); + + Stopwatch Timer; + + // Poll to check when the agent is ready + + do + { + Sleep(100); + HttpClient::Response Resp = m_HttpClient.Get("v1/status/leader"); + if (Resp) + { + ZEN_INFO("Consul agent started successfully (waited {})", NiceTimeSpanMs(Timer.GetElapsedTimeMs())); + + return; + } + } while (Timer.GetElapsedTimeMs() < 10000); + } + + // Report failure! + + ZEN_WARN("Consul agent failed to start within timeout period"); + } + + void StopConsulAgent() + { + if (!m_ProcessHandle.IsValid()) + { + return; + } + + // This waits for the process to exit and also resets the handle + m_ProcessHandle.Kill(); + } + +private: + ProcessHandle m_ProcessHandle; + HttpClient m_HttpClient; +}; + +ConsulProcess::ConsulProcess() : m_Impl(std::make_unique<Impl>("http://localhost:8500/")) +{ +} + +ConsulProcess::~ConsulProcess() +{ +} + +void +ConsulProcess::SpawnConsulAgent() +{ + m_Impl->SpawnConsulAgent(); +} + +void +ConsulProcess::StopConsulAgent() +{ + m_Impl->StopConsulAgent(); +} + +////////////////////////////////////////////////////////////////////////// + +ConsulClient::ConsulClient(std::string_view BaseUri) : m_HttpClient(BaseUri) +{ +} + +ConsulClient::~ConsulClient() +{ +} + +void +ConsulClient::SetKeyValue(std::string_view Key, std::string_view Value) +{ + IoBuffer ValueBuffer = IoBufferBuilder::MakeFromMemory(MakeMemoryView(Value)); + HttpClient::Response Result = + m_HttpClient.Put(fmt::format("v1/kv/{}", Key), ValueBuffer, {{"Content-Type", "text/plain"}, {"Accept", "application/json"}}); + if (!Result) + { + throw runtime_error("ConsulClient::SetKeyValue() failed to set key '{}' ({})", Key, Result.ErrorMessage("")); + } +} + +std::string +ConsulClient::GetKeyValue(std::string_view Key) +{ + HttpClient::Response Result = m_HttpClient.Get(fmt::format("v1/kv/{}?raw", Key)); + if (!Result) + { + throw runtime_error("ConsulClient::GetKeyValue() failed to get key '{}' ({})", Key, Result.ErrorMessage("")); + } + return Result.ToText(); +} + +void +ConsulClient::DeleteKey(std::string_view Key) +{ + HttpClient::Response Result = m_HttpClient.Delete(fmt::format("v1/kv/{}", Key)); + if (!Result) + { + throw runtime_error("ConsulClient::DeleteKey() failed to delete key '{}' ({})", Key, Result.ErrorMessage("")); + } +} + +} // namespace zen::consul diff --git a/src/zenutil/include/zenutil/consul.h b/src/zenutil/include/zenutil/consul.h new file mode 100644 index 000000000..08871fa66 --- /dev/null +++ b/src/zenutil/include/zenutil/consul.h @@ -0,0 +1,47 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include <zenbase/zenbase.h> +#include <zenhttp/httpclient.h> + +#include <string> +#include <string_view> + +namespace zen::consul { + +class ConsulClient +{ +public: + ConsulClient(std::string_view BaseUri); + ~ConsulClient(); + + ConsulClient(const ConsulClient&) = delete; + ConsulClient& operator=(const ConsulClient&) = delete; + + void SetKeyValue(std::string_view Key, std::string_view Value); + std::string GetKeyValue(std::string_view Key); + void DeleteKey(std::string_view Key); + +private: + HttpClient m_HttpClient; +}; + +class ConsulProcess +{ +public: + ConsulProcess(); + ~ConsulProcess(); + + ConsulProcess(const ConsulProcess&) = delete; + ConsulProcess& operator=(const ConsulProcess&) = delete; + + void SpawnConsulAgent(); + void StopConsulAgent(); + +private: + struct Impl; + std::unique_ptr<Impl> m_Impl; +}; + +} // namespace zen::consul |