aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-01-19 13:14:42 +0100
committerGitHub Enterprise <[email protected]>2026-01-19 13:14:42 +0100
commit5081a33dcd19c5d6f9d9cdae9f8745ba786374d4 (patch)
tree6733e6f69137afc690a8c6dcdf618e69ce8029c5 /src
parentMerge pull request #701 from ue-foundation/lm/mac-daemon-mode (diff)
downloadzen-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.cpp2
-rw-r--r--src/zencore/include/zencore/except_fmt.h36
-rw-r--r--src/zencore/include/zencore/filesystem.h2
-rw-r--r--src/zencore/include/zencore/process.h61
-rw-r--r--src/zencore/include/zencore/string.h16
-rw-r--r--src/zencore/process.cpp142
-rw-r--r--src/zenutil/consul/consul.cpp140
-rw-r--r--src/zenutil/include/zenutil/consul.h47
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