aboutsummaryrefslogtreecommitdiff
path: root/src/zencore
diff options
context:
space:
mode:
Diffstat (limited to 'src/zencore')
-rw-r--r--src/zencore/basicfile.cpp117
-rw-r--r--src/zencore/compactbinaryjson.cpp34
-rw-r--r--src/zencore/compactbinarypackage.cpp2
-rw-r--r--src/zencore/crashhandler.cpp2
-rw-r--r--src/zencore/crypto.cpp389
-rw-r--r--src/zencore/filesystem.cpp406
-rw-r--r--src/zencore/include/zencore/basicfile.h5
-rw-r--r--src/zencore/include/zencore/blockingqueue.h7
-rw-r--r--src/zencore/include/zencore/crypto.h16
-rw-r--r--src/zencore/include/zencore/filesystem.h65
-rw-r--r--src/zencore/include/zencore/fmtutils.h67
-rw-r--r--src/zencore/include/zencore/hashutils.h19
-rw-r--r--src/zencore/include/zencore/intmath.h32
-rw-r--r--src/zencore/include/zencore/iobuffer.h19
-rw-r--r--src/zencore/include/zencore/iohash.h1
-rw-r--r--src/zencore/include/zencore/logbase.h15
-rw-r--r--src/zencore/include/zencore/logging.h65
-rw-r--r--src/zencore/include/zencore/logging/broadcastsink.h4
-rw-r--r--src/zencore/include/zencore/logging/helpers.h2
-rw-r--r--src/zencore/include/zencore/logging/logmsg.h49
-rw-r--r--src/zencore/include/zencore/logging/tracelog.h63
-rw-r--r--src/zencore/include/zencore/logging/tracesink.h27
-rw-r--r--src/zencore/include/zencore/mpscqueue.h2
-rw-r--r--src/zencore/include/zencore/process.h87
-rw-r--r--src/zencore/include/zencore/sentryintegration.h15
-rw-r--r--src/zencore/include/zencore/sharedbuffer.h8
-rw-r--r--src/zencore/include/zencore/string.h125
-rw-r--r--src/zencore/include/zencore/system.h5
-rw-r--r--src/zencore/include/zencore/testutils.h22
-rw-r--r--src/zencore/include/zencore/thread.h2
-rw-r--r--src/zencore/include/zencore/trace.h49
-rw-r--r--src/zencore/include/zencore/zencore.h2
-rw-r--r--src/zencore/intmath.cpp94
-rw-r--r--src/zencore/iobuffer.cpp13
-rw-r--r--src/zencore/jobqueue.cpp27
-rw-r--r--src/zencore/logging.cpp33
-rw-r--r--src/zencore/logging/ansicolorsink.cpp68
-rw-r--r--src/zencore/logging/logger.cpp7
-rw-r--r--src/zencore/logging/registry.cpp4
-rw-r--r--src/zencore/logging/tracelog.cpp800
-rw-r--r--src/zencore/logging/tracesink.cpp92
-rw-r--r--src/zencore/memory/memory.cpp4
-rw-r--r--src/zencore/process.cpp725
-rw-r--r--src/zencore/refcount.cpp24
-rw-r--r--src/zencore/sentryintegration.cpp21
-rw-r--r--src/zencore/sharedbuffer.cpp2
-rw-r--r--src/zencore/string.cpp154
-rw-r--r--src/zencore/system.cpp97
-rw-r--r--src/zencore/testing.cpp6
-rw-r--r--src/zencore/testutils.cpp12
-rw-r--r--src/zencore/thread.cpp16
-rw-r--r--src/zencore/trace.cpp53
-rw-r--r--src/zencore/workthreadpool.cpp8
-rw-r--r--src/zencore/xmake.lua13
54 files changed, 3372 insertions, 624 deletions
diff --git a/src/zencore/basicfile.cpp b/src/zencore/basicfile.cpp
index 9dcf7663a..fdf742261 100644
--- a/src/zencore/basicfile.cpp
+++ b/src/zencore/basicfile.cpp
@@ -3,6 +3,7 @@
#include <zencore/basicfile.h>
#include <zencore/compactbinary.h>
+#include <zencore/compactbinarybuilder.h>
#include <zencore/except.h>
#include <zencore/filesystem.h>
#include <zencore/fmtutils.h>
@@ -608,6 +609,67 @@ LockFile::Update(CbObject Payload, std::error_code& Ec)
BasicFile::Write(Payload.GetBuffer(), 0, Ec);
}
+bool
+LockFile::IsHeldLive(const std::filesystem::path& FileName, bool AttemptCleanup)
+{
+#if ZEN_PLATFORM_WINDOWS
+ if (AttemptCleanup)
+ {
+ if (DeleteFileW(FileName.c_str()))
+ {
+ return false;
+ }
+ const DWORD Err = ::GetLastError();
+ if (Err == ERROR_FILE_NOT_FOUND || Err == ERROR_PATH_NOT_FOUND)
+ {
+ return false;
+ }
+ return true;
+ }
+
+ HANDLE Handle = CreateFileW(FileName.c_str(),
+ DELETE,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ nullptr,
+ OPEN_EXISTING,
+ FILE_ATTRIBUTE_NORMAL,
+ nullptr);
+ if (Handle != INVALID_HANDLE_VALUE)
+ {
+ CloseHandle(Handle);
+ return false;
+ }
+ const DWORD Err = ::GetLastError();
+ if (Err == ERROR_FILE_NOT_FOUND || Err == ERROR_PATH_NOT_FOUND)
+ {
+ return false;
+ }
+ return true;
+#else
+ int Fd = open(FileName.c_str(), O_RDONLY | O_CLOEXEC);
+ if (Fd < 0)
+ {
+ if (errno == ENOENT)
+ {
+ return false;
+ }
+ return true;
+ }
+ if (flock(Fd, LOCK_EX | LOCK_NB) == 0)
+ {
+ if (AttemptCleanup)
+ {
+ unlink(FileName.c_str());
+ }
+ flock(Fd, LOCK_UN);
+ close(Fd);
+ return false;
+ }
+ close(Fd);
+ return true;
+#endif
+}
+
//////////////////////////////////////////////////////////////////////////
BasicFileBuffer::BasicFileBuffer(BasicFile& Base, uint64_t BufferSize)
@@ -947,6 +1009,61 @@ TEST_CASE("TemporaryFile")
}
}
+TEST_CASE("LockFile.IsHeldLive")
+{
+ ScopedCurrentDirectoryChange _;
+ const std::filesystem::path Path = std::filesystem::current_path() / "heldlive.lock";
+
+ SUBCASE("Nonexistent")
+ {
+ std::error_code Ec;
+ RemoveFile(Path, Ec);
+ CHECK(LockFile::IsHeldLive(Path, /*AttemptCleanup*/ true) == false);
+ CHECK(LockFile::IsHeldLive(Path, /*AttemptCleanup*/ false) == false);
+ CHECK(IsFile(Path) == false);
+ }
+
+ SUBCASE("Live holder")
+ {
+ LockFile Lock;
+ std::error_code Ec;
+ Lock.Create(Path, CbObjectWriter().Save(), Ec);
+ CHECK(!Ec);
+ CHECK(IsFile(Path));
+
+ CHECK(LockFile::IsHeldLive(Path, /*AttemptCleanup*/ false) == true);
+ CHECK(IsFile(Path));
+ CHECK(LockFile::IsHeldLive(Path, /*AttemptCleanup*/ true) == true);
+ CHECK(IsFile(Path));
+ }
+
+ SUBCASE("Stale file, cleanup")
+ {
+ {
+ BasicFile File;
+ File.Open(Path, BasicFile::Mode::kTruncate);
+ File.Write("x", 1, 0);
+ }
+ CHECK(IsFile(Path));
+ CHECK(LockFile::IsHeldLive(Path, /*AttemptCleanup*/ true) == false);
+ CHECK(IsFile(Path) == false);
+ }
+
+ SUBCASE("Stale file, no cleanup")
+ {
+ {
+ BasicFile File;
+ File.Open(Path, BasicFile::Mode::kTruncate);
+ File.Write("x", 1, 0);
+ }
+ CHECK(IsFile(Path));
+ CHECK(LockFile::IsHeldLive(Path, /*AttemptCleanup*/ false) == false);
+ CHECK(IsFile(Path));
+ std::error_code Ec;
+ RemoveFile(Path, Ec);
+ }
+}
+
TEST_CASE("BasicFileBuffer")
{
ScopedCurrentDirectoryChange _;
diff --git a/src/zencore/compactbinaryjson.cpp b/src/zencore/compactbinaryjson.cpp
index da560a449..5bfbd5e3e 100644
--- a/src/zencore/compactbinaryjson.cpp
+++ b/src/zencore/compactbinaryjson.cpp
@@ -11,6 +11,8 @@
#include <zencore/testing.h>
#include <fmt/format.h>
+#include <cmath>
+#include <limits>
#include <vector>
ZEN_THIRD_PARTY_INCLUDES_START
@@ -570,13 +572,37 @@ private:
break;
case Json::Type::NUMBER:
{
- if (FieldName.empty())
- {
- Writer.AddFloat(Json.number_value());
+ // If the JSON number has no fractional part and fits in an int64,
+ // store it as an integer so that AsInt32/AsInt64 work without
+ // requiring callers to go through AsFloat.
+ double Value = Json.number_value();
+ double IntPart;
+ bool IsIntegral = (std::modf(Value, &IntPart) == 0.0) &&
+ Value >= static_cast<double>(std::numeric_limits<int64_t>::min()) &&
+ Value <= static_cast<double>(std::numeric_limits<int64_t>::max());
+
+ if (IsIntegral)
+ {
+ int64_t IntValue = static_cast<int64_t>(Value);
+ if (FieldName.empty())
+ {
+ Writer.AddInteger(IntValue);
+ }
+ else
+ {
+ Writer.AddInteger(FieldName, IntValue);
+ }
}
else
{
- Writer.AddFloat(FieldName, Json.number_value());
+ if (FieldName.empty())
+ {
+ Writer.AddFloat(Value);
+ }
+ else
+ {
+ Writer.AddFloat(FieldName, Value);
+ }
}
}
break;
diff --git a/src/zencore/compactbinarypackage.cpp b/src/zencore/compactbinarypackage.cpp
index cd268745c..87b58baf7 100644
--- a/src/zencore/compactbinarypackage.cpp
+++ b/src/zencore/compactbinarypackage.cpp
@@ -1410,7 +1410,7 @@ TEST_CASE("cbpackage.legacy.hashresolution")
CbPackage LoadedPkg;
CHECK(legacy::TryLoadCbPackage(LoadedPkg, Buffer, &UniqueBuffer::Alloc));
- // The hash-only path requires a mapper — without one it should fail
+ // The hash-only path requires a mapper - without one it should fail
CbWriter HashOnlyCb;
HashOnlyCb.AddHash(ObjectAttach.GetHash());
HashOnlyCb.AddNull();
diff --git a/src/zencore/crashhandler.cpp b/src/zencore/crashhandler.cpp
index 31b8e6ce2..14904a4b2 100644
--- a/src/zencore/crashhandler.cpp
+++ b/src/zencore/crashhandler.cpp
@@ -56,7 +56,7 @@ CrashExceptionFilter(PEXCEPTION_POINTERS ExceptionInfo)
HANDLE Process = GetCurrentProcess();
HANDLE Thread = GetCurrentThread();
- // SymInitialize is safe to call if already initialized — it returns FALSE
+ // SymInitialize is safe to call if already initialized - it returns FALSE
// but existing state remains valid for SymFromAddr calls
SymInitialize(Process, NULL, TRUE);
diff --git a/src/zencore/crypto.cpp b/src/zencore/crypto.cpp
index 049854b42..9984f35ac 100644
--- a/src/zencore/crypto.cpp
+++ b/src/zencore/crypto.cpp
@@ -6,6 +6,7 @@
#include <zencore/scopeguard.h>
#include <zencore/testing.h>
+#include <array>
#include <string>
#include <string_view>
@@ -47,14 +48,27 @@ ZEN_THIRD_PARTY_INCLUDES_START
# include <openssl/conf.h>
# include <openssl/err.h>
# include <openssl/evp.h>
+# include <openssl/rand.h>
#elif ZEN_USE_MBEDTLS
# include <mbedtls/cipher.h>
+# include <mbedtls/ctr_drbg.h>
+# include <mbedtls/entropy.h>
#else
# include <zencore/windows.h>
# include <bcrypt.h>
# define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
# define STATUS_UNSUCCESSFUL ((NTSTATUS)0xC0000001L)
#endif
+
+#if ZEN_PLATFORM_WINDOWS
+# include <zencore/windows.h>
+# include <dpapi.h>
+#elif ZEN_PLATFORM_MAC
+# include <CoreFoundation/CoreFoundation.h>
+# include <Security/Security.h>
+#elif ZEN_PLATFORM_LINUX && ZEN_USE_LIBSECRET
+# include <libsecret/secret.h>
+#endif
ZEN_THIRD_PARTY_INCLUDES_END
namespace zen {
@@ -420,6 +434,291 @@ namespace crypto {
} // namespace crypto
+bool
+SecureRandomBytes(MutableMemoryView Out)
+{
+ if (Out.GetSize() == 0)
+ {
+ return true;
+ }
+
+#if ZEN_USE_BCRYPT
+ NTSTATUS Status =
+ BCryptGenRandom(nullptr, static_cast<PUCHAR>(Out.GetData()), static_cast<ULONG>(Out.GetSize()), BCRYPT_USE_SYSTEM_PREFERRED_RNG);
+ return NT_SUCCESS(Status);
+#elif ZEN_USE_OPENSSL
+ return RAND_bytes(static_cast<unsigned char*>(Out.GetData()), static_cast<int>(Out.GetSize())) == 1;
+#else // ZEN_USE_MBEDTLS
+ mbedtls_entropy_context Entropy;
+ mbedtls_ctr_drbg_context Drbg;
+ mbedtls_entropy_init(&Entropy);
+ mbedtls_ctr_drbg_init(&Drbg);
+ auto _ = MakeGuard([&]() {
+ mbedtls_ctr_drbg_free(&Drbg);
+ mbedtls_entropy_free(&Entropy);
+ });
+ if (mbedtls_ctr_drbg_seed(&Drbg, mbedtls_entropy_func, &Entropy, nullptr, 0) != 0)
+ {
+ return false;
+ }
+ return mbedtls_ctr_drbg_random(&Drbg, static_cast<unsigned char*>(Out.GetData()), Out.GetSize()) == 0;
+#endif
+}
+
+#if ZEN_PLATFORM_MAC || (ZEN_PLATFORM_LINUX && ZEN_USE_LIBSECRET)
+namespace {
+ // Keychain-backed wrap/unwrap. Plaintext is stored in the OS keyring under a
+ // fixed service and a per-call random account id. The "wrapped" blob returned to
+ // the caller is just the account id bytes; the plaintext never lands on disk.
+ constexpr size_t kKeychainAccountBytes = 16;
+ constexpr char kKeychainServiceName[] = "org.unrealengine.zen.auth";
+
+ bool AccountStringFromBytes(const uint8_t* Bytes, size_t Size, char* OutHex, size_t OutHexSize)
+ {
+ if (OutHexSize < Size * 2 + 1)
+ {
+ return false;
+ }
+ for (size_t i = 0; i < Size; ++i)
+ {
+ static const char kHex[] = "0123456789abcdef";
+ OutHex[i * 2] = kHex[Bytes[i] >> 4];
+ OutHex[i * 2 + 1] = kHex[Bytes[i] & 0x0F];
+ }
+ OutHex[Size * 2] = '\0';
+ return true;
+ }
+} // namespace
+#endif
+
+#if ZEN_PLATFORM_LINUX && ZEN_USE_LIBSECRET
+namespace {
+ // Schema name is shared across zen installs on the machine — uniqueness per
+ // install comes from the random `account` attribute.
+ const SecretSchema* ZenAuthSchema()
+ {
+ static const SecretSchema Schema = {"org.unrealengine.zen.AuthMachineKey",
+ SECRET_SCHEMA_NONE,
+ {
+ {"account", SECRET_SCHEMA_ATTRIBUTE_STRING},
+ {nullptr, SecretSchemaAttributeType(0)},
+ },
+ // reserved
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0};
+ return &Schema;
+ }
+} // namespace
+#endif
+
+bool
+TryProtectData(MemoryView Plaintext, std::vector<uint8_t>& OutProtected)
+{
+#if ZEN_PLATFORM_WINDOWS
+ DATA_BLOB In{static_cast<DWORD>(Plaintext.GetSize()), const_cast<BYTE*>(static_cast<const BYTE*>(Plaintext.GetData()))};
+ DATA_BLOB Out{};
+ if (!CryptProtectData(&In, L"zen auth machine key", nullptr, nullptr, nullptr, 0, &Out))
+ {
+ return false;
+ }
+ auto _ = MakeGuard([&Out]() { LocalFree(Out.pbData); });
+ OutProtected.assign(Out.pbData, Out.pbData + Out.cbData);
+ return true;
+#elif ZEN_PLATFORM_MAC
+ uint8_t AccountBytes[kKeychainAccountBytes];
+ if (!SecureRandomBytes(MutableMemoryView(AccountBytes, sizeof(AccountBytes))))
+ {
+ return false;
+ }
+ char AccountHex[sizeof(AccountBytes) * 2 + 1];
+ AccountStringFromBytes(AccountBytes, sizeof(AccountBytes), AccountHex, sizeof(AccountHex));
+
+ CFStringRef Account = CFStringCreateWithCString(nullptr, AccountHex, kCFStringEncodingASCII);
+ if (Account == nullptr)
+ {
+ return false;
+ }
+ auto _A = MakeGuard([&]() { CFRelease(Account); });
+
+ CFDataRef Secret = CFDataCreate(nullptr, static_cast<const UInt8*>(Plaintext.GetData()), static_cast<CFIndex>(Plaintext.GetSize()));
+ if (Secret == nullptr)
+ {
+ return false;
+ }
+ auto _S = MakeGuard([&]() { CFRelease(Secret); });
+
+ const void* Keys[] = {kSecClass, kSecAttrService, kSecAttrAccount, kSecValueData, kSecAttrAccessible};
+ const void* Values[] = {kSecClassGenericPassword,
+ CFSTR("org.unrealengine.zen.auth"),
+ Account,
+ Secret,
+ kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly};
+ CFDictionaryRef Attrs = CFDictionaryCreate(nullptr,
+ Keys,
+ Values,
+ sizeof(Keys) / sizeof(*Keys),
+ &kCFTypeDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks);
+ if (Attrs == nullptr)
+ {
+ return false;
+ }
+ auto _D = MakeGuard([&]() { CFRelease(Attrs); });
+
+ OSStatus Status = SecItemAdd(Attrs, nullptr);
+ if (Status != errSecSuccess)
+ {
+ return false;
+ }
+ OutProtected.assign(AccountBytes, AccountBytes + sizeof(AccountBytes));
+ return true;
+#elif ZEN_PLATFORM_LINUX && ZEN_USE_LIBSECRET
+ uint8_t AccountBytes[kKeychainAccountBytes];
+ if (!SecureRandomBytes(MutableMemoryView(AccountBytes, sizeof(AccountBytes))))
+ {
+ return false;
+ }
+ char AccountHex[sizeof(AccountBytes) * 2 + 1];
+ AccountStringFromBytes(AccountBytes, sizeof(AccountBytes), AccountHex, sizeof(AccountHex));
+
+ // libsecret password APIs take UTF-8 strings, so base64 the raw key material
+ // before handing it to the keyring. Decoded back on lookup.
+ gchar* Encoded = g_base64_encode(static_cast<const guchar*>(Plaintext.GetData()), Plaintext.GetSize());
+ if (Encoded == nullptr)
+ {
+ return false;
+ }
+ auto _E = MakeGuard([&]() { g_free(Encoded); });
+
+ GError* Err = nullptr;
+ gboolean Ok = secret_password_store_sync(ZenAuthSchema(),
+ SECRET_COLLECTION_DEFAULT,
+ "zen auth machine key",
+ Encoded,
+ nullptr,
+ &Err,
+ "account",
+ AccountHex,
+ nullptr);
+ if (Err != nullptr)
+ {
+ g_error_free(Err);
+ }
+ if (!Ok)
+ {
+ return false;
+ }
+ OutProtected.assign(AccountBytes, AccountBytes + sizeof(AccountBytes));
+ return true;
+#else
+ (void)Plaintext;
+ (void)OutProtected;
+ return false;
+#endif
+}
+
+bool
+TryUnprotectData(MemoryView Protected, std::vector<uint8_t>& OutPlaintext)
+{
+#if ZEN_PLATFORM_WINDOWS
+ DATA_BLOB In{static_cast<DWORD>(Protected.GetSize()), const_cast<BYTE*>(static_cast<const BYTE*>(Protected.GetData()))};
+ DATA_BLOB Out{};
+ if (!CryptUnprotectData(&In, nullptr, nullptr, nullptr, nullptr, 0, &Out))
+ {
+ return false;
+ }
+ auto _ = MakeGuard([&Out]() { LocalFree(Out.pbData); });
+ OutPlaintext.assign(Out.pbData, Out.pbData + Out.cbData);
+ return true;
+#elif ZEN_PLATFORM_MAC
+ if (Protected.GetSize() != kKeychainAccountBytes)
+ {
+ return false;
+ }
+ char AccountHex[kKeychainAccountBytes * 2 + 1];
+ AccountStringFromBytes(static_cast<const uint8_t*>(Protected.GetData()), kKeychainAccountBytes, AccountHex, sizeof(AccountHex));
+
+ CFStringRef Account = CFStringCreateWithCString(nullptr, AccountHex, kCFStringEncodingASCII);
+ if (Account == nullptr)
+ {
+ return false;
+ }
+ auto _A = MakeGuard([&]() { CFRelease(Account); });
+
+ const void* Keys[] = {kSecClass, kSecAttrService, kSecAttrAccount, kSecReturnData, kSecMatchLimit};
+ const void* Values[] = {kSecClassGenericPassword, CFSTR("org.unrealengine.zen.auth"), Account, kCFBooleanTrue, kSecMatchLimitOne};
+ CFDictionaryRef Query = CFDictionaryCreate(nullptr,
+ Keys,
+ Values,
+ sizeof(Keys) / sizeof(*Keys),
+ &kCFTypeDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks);
+ if (Query == nullptr)
+ {
+ return false;
+ }
+ auto _D = MakeGuard([&]() { CFRelease(Query); });
+
+ CFTypeRef Result = nullptr;
+ OSStatus Status = SecItemCopyMatching(Query, &Result);
+ if (Status != errSecSuccess || Result == nullptr)
+ {
+ return false;
+ }
+ auto _R = MakeGuard([&]() { CFRelease(Result); });
+
+ if (CFGetTypeID(Result) != CFDataGetTypeID())
+ {
+ return false;
+ }
+ CFDataRef Data = static_cast<CFDataRef>(Result);
+ const UInt8* DataPtr = CFDataGetBytePtr(Data);
+ const CFIndex DataLen = CFDataGetLength(Data);
+ OutPlaintext.assign(DataPtr, DataPtr + DataLen);
+ return true;
+#elif ZEN_PLATFORM_LINUX && ZEN_USE_LIBSECRET
+ if (Protected.GetSize() != kKeychainAccountBytes)
+ {
+ return false;
+ }
+ char AccountHex[kKeychainAccountBytes * 2 + 1];
+ AccountStringFromBytes(static_cast<const uint8_t*>(Protected.GetData()), kKeychainAccountBytes, AccountHex, sizeof(AccountHex));
+
+ GError* Err = nullptr;
+ gchar* Encoded = secret_password_lookup_sync(ZenAuthSchema(), nullptr, &Err, "account", AccountHex, nullptr);
+ if (Err != nullptr)
+ {
+ g_error_free(Err);
+ }
+ if (Encoded == nullptr)
+ {
+ return false;
+ }
+ // `secret_password_free` zeroes memory before freeing; use it rather than g_free.
+ auto _E = MakeGuard([&]() { secret_password_free(Encoded); });
+
+ gsize DecodedLen = 0;
+ guchar* Decoded = g_base64_decode(Encoded, &DecodedLen);
+ if (Decoded == nullptr)
+ {
+ return false;
+ }
+ auto _D = MakeGuard([&]() { g_free(Decoded); });
+
+ OutPlaintext.assign(Decoded, Decoded + DecodedLen);
+ return true;
+#else
+ (void)Protected;
+ (void)OutPlaintext;
+ return false;
+#endif
+}
+
MemoryView
Aes::Encrypt(const AesKey256Bit& Key, const AesIV128Bit& IV, MemoryView In, MutableMemoryView Out, std::optional<std::string>& Reason)
{
@@ -502,6 +801,96 @@ TEST_CASE("crypto.aes")
}
}
+TEST_CASE("crypto.securerandom")
+{
+ std::array<uint8_t, 64> A{};
+ std::array<uint8_t, 64> B{};
+ CHECK(SecureRandomBytes(MutableMemoryView(A.data(), A.size())));
+ CHECK(SecureRandomBytes(MutableMemoryView(B.data(), B.size())));
+ // Vanishingly small probability two 64-byte draws match.
+ CHECK(A != B);
+}
+
+TEST_CASE("crypto.protectdata")
+{
+ const uint8_t Plain[48] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
+ 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48};
+ std::vector<uint8_t> Wrapped;
+ const bool WrapOk = TryProtectData(MakeMemoryView(Plain), Wrapped);
+# if ZEN_PLATFORM_WINDOWS
+ REQUIRE(WrapOk);
+ CHECK(Wrapped.size() != sizeof(Plain)); // DPAPI envelope adds overhead
+ CHECK(memcmp(Wrapped.data(), Plain, std::min(Wrapped.size(), sizeof(Plain))) != 0);
+
+ std::vector<uint8_t> Unwrapped;
+ REQUIRE(TryUnprotectData(MakeMemoryView(Wrapped), Unwrapped));
+ CHECK(Unwrapped.size() == sizeof(Plain));
+ CHECK(memcmp(Unwrapped.data(), Plain, sizeof(Plain)) == 0);
+# elif ZEN_PLATFORM_MAC
+ // Keychain may not be accessible in headless / CI contexts. Round-trip is
+ // asserted only when Wrap succeeds; otherwise the test is a no-op.
+ if (WrapOk)
+ {
+ CHECK(Wrapped.size() == 16); // Keychain account id
+ std::vector<uint8_t> Unwrapped;
+ REQUIRE(TryUnprotectData(MakeMemoryView(Wrapped), Unwrapped));
+ CHECK(Unwrapped.size() == sizeof(Plain));
+ CHECK(memcmp(Unwrapped.data(), Plain, sizeof(Plain)) == 0);
+
+ // Delete the Keychain entry we just added so tests don't accumulate residue.
+ static const char kHex[] = "0123456789abcdef";
+ char AccountHex[33];
+ for (size_t i = 0; i < 16; ++i)
+ {
+ AccountHex[i * 2] = kHex[Wrapped[i] >> 4];
+ AccountHex[i * 2 + 1] = kHex[Wrapped[i] & 0x0F];
+ }
+ AccountHex[32] = '\0';
+ CFStringRef Account = CFStringCreateWithCString(nullptr, AccountHex, kCFStringEncodingASCII);
+ const void* Keys[] = {kSecClass, kSecAttrService, kSecAttrAccount};
+ const void* Values[] = {kSecClassGenericPassword, CFSTR("org.unrealengine.zen.auth"), Account};
+ CFDictionaryRef Query =
+ CFDictionaryCreate(nullptr, Keys, Values, 3, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+ SecItemDelete(Query);
+ CFRelease(Query);
+ CFRelease(Account);
+ }
+# elif ZEN_PLATFORM_LINUX && ZEN_USE_LIBSECRET
+ // libsecret round-trip only when a Secret Service daemon is reachable. On
+ // headless / container CI with no D-Bus session, WrapOk is false and we fall
+ // back to the raw-bytes path at the authutils level, so the test is a no-op.
+ if (WrapOk)
+ {
+ CHECK(Wrapped.size() == 16);
+ std::vector<uint8_t> Unwrapped;
+ REQUIRE(TryUnprotectData(MakeMemoryView(Wrapped), Unwrapped));
+ CHECK(Unwrapped.size() == sizeof(Plain));
+ CHECK(memcmp(Unwrapped.data(), Plain, sizeof(Plain)) == 0);
+
+ // Clean up the keyring entry we just created.
+ static const char kHex[] = "0123456789abcdef";
+ char AccountHex[33];
+ for (size_t i = 0; i < 16; ++i)
+ {
+ AccountHex[i * 2] = kHex[Wrapped[i] >> 4];
+ AccountHex[i * 2 + 1] = kHex[Wrapped[i] & 0x0F];
+ }
+ AccountHex[32] = '\0';
+ GError* Err = nullptr;
+ secret_password_clear_sync(ZenAuthSchema(), nullptr, &Err, "account", AccountHex, nullptr);
+ if (Err != nullptr)
+ {
+ g_error_free(Err);
+ }
+ }
+# else
+ // No OS-level implementation compiled in; both calls must report failure.
+ CHECK(WrapOk == false);
+ std::vector<uint8_t> Unwrapped;
+ CHECK(TryUnprotectData(MakeMemoryView(Plain), Unwrapped) == false);
+# endif
+}
+
TEST_SUITE_END();
#endif
diff --git a/src/zencore/filesystem.cpp b/src/zencore/filesystem.cpp
index a63594be9..281cb8e2e 100644
--- a/src/zencore/filesystem.cpp
+++ b/src/zencore/filesystem.cpp
@@ -114,6 +114,20 @@ struct ScopedFd
explicit operator bool() const { return Fd >= 0; }
};
+# if ZEN_PLATFORM_LINUX
+inline uint64_t
+StatMtime100Ns(const struct stat& S)
+{
+ return uint64_t(S.st_mtim.tv_sec) * 10'000'000ULL + uint64_t(S.st_mtim.tv_nsec) / 100;
+}
+# elif ZEN_PLATFORM_MAC
+inline uint64_t
+StatMtime100Ns(const struct stat& S)
+{
+ return uint64_t(S.st_mtimespec.tv_sec) * 10'000'000ULL + uint64_t(S.st_mtimespec.tv_nsec) / 100;
+}
+# endif
+
#endif // ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
#if ZEN_PLATFORM_WINDOWS
@@ -1092,8 +1106,6 @@ TryCloneFile(void* SourceNativeHandle, void* TargetNativeHandle)
FILE_DISPOSITION_INFO FileDisposition = {TRUE};
if (!SetFileInformationByHandle(TargetFile, FileDispositionInfo, &FileDisposition, sizeof FileDisposition))
{
- const DWORD ErrorCode = ::GetLastError();
- SetLastError(ErrorCode);
return false;
}
@@ -1185,7 +1197,7 @@ TryCloneFile(void* SourceNativeHandle, void* TargetNativeHandle)
}
#endif // ZEN_PLATFORM_WINDOWS
-bool
+std::error_code
TryCloneFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToPath)
{
#if ZEN_PLATFORM_WINDOWS
@@ -1199,7 +1211,7 @@ TryCloneFile(const std::filesystem::path& FromPath, const std::filesystem::path&
if (FromFile == INVALID_HANDLE_VALUE)
{
FromFile.Detach();
- return false;
+ return MakeErrorCodeFromLastError();
}
SetFileAttributesW(ToPath.c_str(), FILE_ATTRIBUTE_NORMAL);
@@ -1215,16 +1227,20 @@ TryCloneFile(const std::filesystem::path& FromPath, const std::filesystem::path&
if (TargetFile == INVALID_HANDLE_VALUE)
{
TargetFile.Detach();
- return false;
+ return MakeErrorCodeFromLastError();
+ }
+ if (!TryCloneFile((void*)FromFile.m_Handle, (void*)TargetFile.m_Handle))
+ {
+ return MakeErrorCodeFromLastError();
}
- return TryCloneFile((void*)FromFile.m_Handle, (void*)TargetFile.m_Handle);
+ return {};
#elif ZEN_PLATFORM_LINUX
// The 'from' file
ScopedFd FromFd(open(FromPath.c_str(), O_RDONLY | O_CLOEXEC));
if (!FromFd)
{
- return false;
+ return MakeErrorCodeFromLastError();
}
// Remove any existing target so we can create a fresh clone
@@ -1234,19 +1250,20 @@ TryCloneFile(const std::filesystem::path& FromPath, const std::filesystem::path&
ScopedFd ToFd(open(ToPath.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0666));
if (!ToFd)
{
- return false;
+ return MakeErrorCodeFromLastError();
}
if (ioctl(ToFd.Fd, FICLONE, FromFd.Fd) != 0)
{
// Clone not supported by this filesystem or files are on different volumes.
// Remove the empty target file we created.
- ToFd = ScopedFd();
+ std::error_code Ec = MakeErrorCodeFromLastError();
+ ToFd = ScopedFd();
unlink(ToPath.c_str());
- return false;
+ return Ec;
}
- return true;
+ return {};
#elif ZEN_PLATFORM_MAC
// Remove any existing target - clonefile() requires the destination not exist
unlink(ToPath.c_str());
@@ -1254,70 +1271,63 @@ TryCloneFile(const std::filesystem::path& FromPath, const std::filesystem::path&
if (clonefile(FromPath.c_str(), ToPath.c_str(), CLONE_NOFOLLOW) != 0)
{
// Clone not supported (non-APFS) or files are on different volumes
- return false;
+ return MakeErrorCodeFromLastError();
}
- return true;
+ return {};
#endif // ZEN_PLATFORM_WINDOWS
}
-void
-CopyFile(const std::filesystem::path& FromPath,
- const std::filesystem::path& ToPath,
- const CopyFileOptions& Options,
- std::error_code& OutErrorCode)
-{
- OutErrorCode.clear();
-
- bool Success = CopyFile(FromPath, ToPath, Options);
-
- if (!Success)
- {
- OutErrorCode = MakeErrorCodeFromLastError();
- }
-}
-
-bool
+std::error_code
CopyFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToPath, const CopyFileOptions& Options)
{
- bool Success = false;
-
if (Options.EnableClone)
{
- Success = TryCloneFile(FromPath.native(), ToPath.native());
- if (Success)
+ std::error_code CloneEc = TryCloneFile(FromPath.native(), ToPath.native());
+ if (CloneEc)
{
- return true;
+ if (Options.MustClone)
+ {
+ ZEN_ERROR("CloneFile() failed for {} -> {}: {}", FromPath, ToPath, CloneEc.message());
+ return CloneEc;
+ }
+ }
+ else
+ {
+ return {};
}
}
-
- if (Options.MustClone)
+ else if (Options.MustClone)
{
- ZEN_ERROR("CloneFile() failed for {} -> {}", FromPath, ToPath);
- return false;
+ ZEN_ERROR("CloneFile() required but not enabled for {} -> {}", FromPath, ToPath);
+ return std::make_error_code(std::errc::invalid_argument);
}
#if ZEN_PLATFORM_WINDOWS
BOOL CancelFlag = FALSE;
- Success = !!::CopyFileExW(FromPath.c_str(),
- ToPath.c_str(),
- /* lpProgressRoutine */ nullptr,
- /* lpData */ nullptr,
- &CancelFlag,
- /* dwCopyFlags */ 0);
+ BOOL Success = ::CopyFileExW(FromPath.c_str(),
+ ToPath.c_str(),
+ /* lpProgressRoutine */ nullptr,
+ /* lpData */ nullptr,
+ &CancelFlag,
+ /* dwCopyFlags */ 0);
+ if (!Success)
+ {
+ return MakeErrorCodeFromLastError();
+ }
#else
// From file
ScopedFd FromFd(open(FromPath.c_str(), O_RDONLY | O_CLOEXEC));
if (!FromFd)
{
- ThrowLastError(fmt::format("failed to open file {}", FromPath));
+ return MakeErrorCodeFromLastError();
}
// To file
ScopedFd ToFd(open(ToPath.c_str(), O_WRONLY | O_CREAT | O_CLOEXEC, 0666));
if (!ToFd)
{
- ThrowLastError(fmt::format("failed to create file {}", ToPath));
+ return MakeErrorCodeFromLastError();
}
fchmod(ToFd.Fd, 0666);
@@ -1330,32 +1340,36 @@ CopyFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToP
ZEN_UNUSED($Ignore); // What's the appropriate error handling here?
// Copy impl
- const size_t BufferSize = Min(FileSizeBytes, 64u << 10);
- void* Buffer = malloc(BufferSize);
+ const size_t BufferSize = Min(FileSizeBytes, 64u << 10);
+ void* Buffer = malloc(BufferSize);
+ std::error_code Result;
while (true)
{
int BytesRead = read(FromFd.Fd, Buffer, BufferSize);
- if (BytesRead <= 0)
+ if (BytesRead < 0)
+ {
+ Result = MakeErrorCodeFromLastError();
+ break;
+ }
+ if (BytesRead == 0)
{
- Success = (BytesRead == 0);
break;
}
if (write(ToFd.Fd, Buffer, BytesRead) != BytesRead)
{
- Success = false;
+ Result = MakeErrorCodeFromLastError();
break;
}
}
free(Buffer);
-#endif // ZEN_PLATFORM_WINDOWS
-
- if (!Success)
+ if (Result)
{
- ThrowLastError(fmt::format("file copy from {} to {} failed", FromPath, ToPath));
+ return Result;
}
+#endif // ZEN_PLATFORM_WINDOWS
- return true;
+ return {};
}
void
@@ -1439,24 +1453,13 @@ CopyTree(std::filesystem::path FromPath, std::filesystem::path ToPath, const Cop
ToPath = TargetPath / File;
}
- try
- {
- if (zen::CopyFile(FromPath, ToPath, CopyOptions))
- {
- ++FileCount;
- ByteCount += FileSize;
- }
- else
- {
- throw std::runtime_error("CopyFile failed in an unexpected way");
- }
- }
- catch (const std::exception& Ex)
+ if (std::error_code CopyEc = zen::CopyFile(FromPath, ToPath, CopyOptions); CopyEc)
{
++FailedFileCount;
-
- throw std::runtime_error(fmt::format("failed to copy '{}' to '{}': '{}'", FromPath, ToPath, Ex.what()));
+ throw std::system_error(CopyEc, fmt::format("failed to copy '{}' to '{}'", FromPath, ToPath));
}
+ ++FileCount;
+ ByteCount += FileSize;
}
}
@@ -1669,17 +1672,17 @@ WriteFile(std::filesystem::path Path, CompositeBuffer InData)
WriteFile(Path, DataPtrs.data(), DataPtrs.size());
}
-bool
+std::error_code
MoveToFile(std::filesystem::path Path, IoBuffer Data)
{
if (!Data.IsWholeFile())
{
- return false;
+ return std::make_error_code(std::errc::invalid_argument);
}
IoBufferFileReference FileRef;
if (!Data.GetFileReference(/* out */ FileRef))
{
- return false;
+ return std::make_error_code(std::errc::invalid_argument);
}
#if ZEN_PLATFORM_WINDOWS
@@ -1695,15 +1698,16 @@ MoveToFile(std::filesystem::path Path, IoBuffer Data)
RenameInfo->FileName[FileName.size()] = 0;
// Try to move file into place
- BOOL Success = SetFileInformationByHandle(ChunkFileHandle, FileRenameInfo, RenameInfo, BufferSize);
+ BOOL Success = SetFileInformationByHandle(ChunkFileHandle, FileRenameInfo, RenameInfo, BufferSize);
+ DWORD LastError = Success ? ERROR_SUCCESS : GetLastError();
if (!Success)
{
- DWORD LastError = GetLastError();
if (LastError == ERROR_PATH_NOT_FOUND)
{
zen::CreateDirectories(Path.parent_path());
- Success = SetFileInformationByHandle(ChunkFileHandle, FileRenameInfo, RenameInfo, BufferSize);
+ Success = SetFileInformationByHandle(ChunkFileHandle, FileRenameInfo, RenameInfo, BufferSize);
+ LastError = Success ? ERROR_SUCCESS : GetLastError();
}
if (!Success && (LastError == ERROR_ACCESS_DENIED))
{
@@ -1721,23 +1725,29 @@ MoveToFile(std::filesystem::path Path, IoBuffer Data)
if (LastError == ERROR_PATH_NOT_FOUND)
{
zen::CreateDirectories(Path.parent_path());
- Success = ::MoveFile(NativeSourcePath, NativeTargetPath);
+ Success = ::MoveFile(NativeSourcePath, NativeTargetPath);
+ LastError = Success ? ERROR_SUCCESS : GetLastError();
}
}
}
+ else
+ {
+ Memory::Free(RenameInfo);
+ return Ec;
+ }
}
}
Memory::Free(RenameInfo);
if (!Success)
{
- return false;
+ return MakeErrorCode(LastError);
}
#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
std::error_code Ec;
std::filesystem::path SourcePath = PathFromHandle(FileRef.FileHandle, Ec);
if (Ec)
{
- return false;
+ return Ec;
}
int Ret = rename(SourcePath.c_str(), Path.c_str());
if (Ret < 0)
@@ -1751,11 +1761,11 @@ MoveToFile(std::filesystem::path Path, IoBuffer Data)
}
if (Ret < 0)
{
- return false;
+ return MakeErrorCodeFromLastError();
}
#endif // ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
Data.SetDeleteOnClose(false);
- return true;
+ return {};
}
IoBuffer
@@ -1884,7 +1894,7 @@ ScanFile(void* NativeHandle,
}
}
-bool
+std::error_code
ScanFile(std::filesystem::path Path, const uint64_t ChunkSize, std::function<void(const void* Data, size_t Size)>&& ProcessFunc)
{
#if ZEN_PLATFORM_WINDOWS
@@ -1898,7 +1908,7 @@ ScanFile(std::filesystem::path Path, const uint64_t ChunkSize, std::function<voi
if (FromFile == INVALID_HANDLE_VALUE)
{
FromFile.Detach();
- return false;
+ return MakeErrorCodeFromLastError();
}
std::vector<uint8_t> ReadBuffer(ChunkSize);
@@ -1910,7 +1920,7 @@ ScanFile(std::filesystem::path Path, const uint64_t ChunkSize, std::function<voi
if (!Success)
{
- throw std::system_error(std::error_code(::GetLastError(), std::system_category()), "file scan failed");
+ return MakeErrorCodeFromLastError();
}
if (dwBytesRead == 0)
@@ -1922,10 +1932,10 @@ ScanFile(std::filesystem::path Path, const uint64_t ChunkSize, std::function<voi
ScopedFd InFd(open(Path.c_str(), O_RDONLY | O_CLOEXEC));
if (!InFd)
{
- return false;
+ return MakeErrorCodeFromLastError();
}
- bool Success = true;
+ std::error_code Result;
void* Buffer = malloc(ChunkSize);
while (true)
@@ -1933,7 +1943,7 @@ ScanFile(std::filesystem::path Path, const uint64_t ChunkSize, std::function<voi
int BytesRead = read(InFd.Fd, Buffer, ChunkSize);
if (BytesRead < 0)
{
- Success = false;
+ Result = MakeErrorCodeFromLastError();
break;
}
@@ -1947,13 +1957,13 @@ ScanFile(std::filesystem::path Path, const uint64_t ChunkSize, std::function<voi
free(Buffer);
- if (!Success)
+ if (Result)
{
- ThrowLastError("file scan failed");
+ return Result;
}
#endif // ZEN_PLATFORM_WINDOWS
- return true;
+ return {};
}
void
@@ -2123,7 +2133,7 @@ FileSystemTraversal::TraverseFileSystem(const std::filesystem::path& RootDir, Tr
}
else if (S_ISREG(Stat.st_mode))
{
- Visitor.VisitFile(RootDir, FileName, Stat.st_size, gsl::narrow<uint32_t>(Stat.st_mode), gsl::narrow<uint64_t>(Stat.st_mtime));
+ Visitor.VisitFile(RootDir, FileName, Stat.st_size, gsl::narrow<uint32_t>(Stat.st_mode), StatMtime100Ns(Stat));
}
else
{
@@ -2507,7 +2517,7 @@ GetModificationTickFromHandle(void* NativeHandle, std::error_code& Ec)
struct stat Stat;
if (0 == fstat(Fd, &Stat))
{
- return gsl::narrow<uint64_t>(Stat.st_mtime);
+ return StatMtime100Ns(Stat);
}
#endif
Ec = MakeErrorCodeFromLastError();
@@ -2546,11 +2556,11 @@ GetModificationTickFromPath(const std::filesystem::path& Filename)
{
ThrowLastError(fmt::format("Failed to get mode of file {}", Filename));
}
- return gsl::narrow<uint64_t>(Stat.st_mtime);
+ return StatMtime100Ns(Stat);
#endif
}
-bool
+std::error_code
TryGetFileProperties(const std::filesystem::path& Path,
uint64_t& OutSize,
uint64_t& OutModificationTick,
@@ -2568,31 +2578,31 @@ TryGetFileProperties(const std::filesystem::path& Path,
nullptr);
if (Handle == INVALID_HANDLE_VALUE)
{
- return false;
+ return MakeErrorCodeFromLastError();
}
auto _ = MakeGuard([Handle]() { CloseHandle(Handle); });
BY_HANDLE_FILE_INFORMATION Bhfh = {};
if (!GetFileInformationByHandle(Handle, &Bhfh))
{
- return false;
+ return MakeErrorCodeFromLastError();
}
OutSize = uint64_t(Bhfh.nFileSizeHigh) << 32 | Bhfh.nFileSizeLow;
OutModificationTick = ((uint64_t(Bhfh.ftLastWriteTime.dwHighDateTime) << 32) | Bhfh.ftLastWriteTime.dwLowDateTime);
OutNativeModeOrAttributes = Bhfh.dwFileAttributes;
- return true;
+ return {};
}
#else
struct stat Stat;
int err = stat(Path.native().c_str(), &Stat);
if (err)
{
- return false;
+ return MakeErrorCodeFromLastError();
}
- OutModificationTick = gsl::narrow<uint64_t>(Stat.st_mtime);
+ OutModificationTick = StatMtime100Ns(Stat);
OutSize = size_t(Stat.st_size);
OutNativeModeOrAttributes = (uint32_t)Stat.st_mode;
- return true;
+ return {};
#endif
}
@@ -2709,10 +2719,10 @@ MaximizeOpenFileCount()
#endif
}
-bool
+std::error_code
PrepareFileForScatteredWrite(void* FileHandle, uint64_t FinalSize)
{
- bool Result = true;
+ std::error_code Result;
#if ZEN_PLATFORM_WINDOWS
BY_HANDLE_FILE_INFORMATION Information;
@@ -2724,9 +2734,9 @@ PrepareFileForScatteredWrite(void* FileHandle, uint64_t FinalSize)
BOOL Ok = DeviceIoControl(FileHandle, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &_, nullptr);
if (!Ok)
{
+ Result = MakeErrorCodeFromLastError();
std::error_code DummyEc;
ZEN_DEBUG("Unable to set sparse mode for file '{}'", PathFromHandle(FileHandle, DummyEc));
- Result = false;
}
}
}
@@ -2735,9 +2745,9 @@ PrepareFileForScatteredWrite(void* FileHandle, uint64_t FinalSize)
AllocationInfo.AllocationSize.QuadPart = LONGLONG(FinalSize);
if (!SetFileInformationByHandle(FileHandle, FileAllocationInfo, &AllocationInfo, DWORD(sizeof(AllocationInfo))))
{
+ Result = MakeErrorCodeFromLastError();
std::error_code DummyEc;
ZEN_DEBUG("Unable to set file allocation size to {} for file '{}'", FinalSize, PathFromHandle(FileHandle, DummyEc));
- Result = false;
}
#else // ZEN_PLATFORM_WINDOWS
@@ -2963,6 +2973,83 @@ GetEnvVariable(std::string_view VariableName)
return "";
}
+std::string
+ExpandEnvironmentVariables(std::string_view Input)
+{
+ std::string Result;
+ Result.reserve(Input.size());
+
+ for (size_t i = 0; i < Input.size(); ++i)
+ {
+ if (Input[i] == '%')
+ {
+ size_t End = Input.find('%', i + 1);
+ if (End != std::string_view::npos && End > i + 1)
+ {
+ std::string_view VarName = Input.substr(i + 1, End - i - 1);
+ std::string Value = GetEnvVariable(VarName);
+ if (!Value.empty())
+ {
+ Result += Value;
+ i = End;
+ continue;
+ }
+ }
+ }
+ Result += Input[i];
+ }
+
+ return Result;
+}
+
+ScopedEnvVar::ScopedEnvVar(std::string_view Name, std::string_view Value) : m_Name(Name)
+{
+#if ZEN_PLATFORM_WINDOWS
+ // Use the raw API so we can distinguish "not set" (ERROR_ENVVAR_NOT_FOUND)
+ // from "set to empty string" (returns 0 with no error).
+ char Buf[1];
+ DWORD Len = GetEnvironmentVariableA(m_Name.c_str(), Buf, sizeof(Buf));
+ if (Len == 0 && GetLastError() == ERROR_ENVVAR_NOT_FOUND)
+ {
+ m_OldValue = std::nullopt;
+ }
+ else
+ {
+ // Len == 0 with no error: variable exists but is empty.
+ // Len > sizeof(Buf): value is non-empty; Len is the required buffer size
+ // (including null terminator) - allocate and re-read.
+ std::string Old(Len == 0 ? 0 : Len - 1, '\0');
+ if (Len > sizeof(Buf))
+ {
+ GetEnvironmentVariableA(m_Name.c_str(), Old.data(), Len);
+ }
+ m_OldValue = std::move(Old);
+ }
+ SetEnvironmentVariableA(m_Name.c_str(), std::string(Value).c_str());
+#else
+ // getenv returns nullptr when not set, "" when set to empty string.
+ const char* Existing = getenv(m_Name.c_str());
+ m_OldValue = Existing ? std::optional<std::string>(Existing) : std::nullopt;
+ setenv(m_Name.c_str(), std::string(Value).c_str(), 1);
+#endif
+}
+
+ScopedEnvVar::~ScopedEnvVar()
+{
+#if ZEN_PLATFORM_WINDOWS
+ SetEnvironmentVariableA(m_Name.c_str(), m_OldValue.has_value() ? m_OldValue->c_str() : nullptr);
+#else
+ if (m_OldValue.has_value())
+ {
+ setenv(m_Name.c_str(), m_OldValue->c_str(), 1);
+ }
+ else
+ {
+ unsetenv(m_Name.c_str());
+ }
+#endif
+}
+
std::error_code
RotateFiles(const std::filesystem::path& Filename, std::size_t MaxFiles)
{
@@ -3092,7 +3179,38 @@ SearchPathForExecutable(std::string_view ExecutableName)
return PathBuffer.get();
#else
- return ExecutableName;
+ // If the name already contains a path separator, don't search PATH
+ // (matches the shell / execvp semantics).
+ if (ExecutableName.find('/') != std::string_view::npos)
+ {
+ return std::filesystem::path(ExecutableName);
+ }
+
+ const char* PathEnv = ::getenv("PATH");
+ if (PathEnv == nullptr || *PathEnv == '\0')
+ {
+ return std::filesystem::path(ExecutableName);
+ }
+
+ std::string_view PathView(PathEnv);
+ while (!PathView.empty())
+ {
+ size_t Sep = PathView.find(':');
+ std::string_view Dir = (Sep == std::string_view::npos) ? PathView : PathView.substr(0, Sep);
+ PathView = (Sep == std::string_view::npos) ? std::string_view{} : PathView.substr(Sep + 1);
+
+ // An empty entry in PATH is interpreted as the current directory.
+ std::filesystem::path Candidate = Dir.empty() ? std::filesystem::path(".") : std::filesystem::path(Dir);
+ Candidate /= ExecutableName;
+
+ std::error_code Ec;
+ if (std::filesystem::is_regular_file(Candidate, Ec) && ::access(Candidate.c_str(), X_OK) == 0)
+ {
+ return Candidate;
+ }
+ }
+
+ return std::filesystem::path(ExecutableName);
#endif
}
@@ -3286,12 +3404,12 @@ MakeSafeAbsolutePathInPlace(std::filesystem::path& Path)
{
if (PathString.starts_with(UncPrefix))
{
- // UNC path: \\server\share → \\?\UNC\server\share
+ // UNC path: \\server\share -> \\?\UNC\server\share
PathString.replace(0, UncPrefix.size(), LongPathUncPrefix);
}
else
{
- // Local path: C:\foo → \\?\C:\foo
+ // Local path: C:\foo -> \\?\C:\foo
PathString.insert(0, LongPathPrefix);
}
Path = PathString;
@@ -3419,7 +3537,7 @@ public:
ZEN_UNUSED(SystemGlobal);
std::string InstanceMapName = fmt::format("/{}", Name);
- ScopedFd FdGuard(shm_open(InstanceMapName.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0666));
+ ScopedFd FdGuard(shm_open(InstanceMapName.c_str(), O_RDWR | O_CREAT, 0666));
if (!FdGuard)
{
return {};
@@ -3591,10 +3709,11 @@ TEST_CASE("filesystem")
// Scan/read file
FileContents BinRead = ReadFile(BinPath);
std::vector<uint8_t> BinScan;
- ScanFile(BinPath, 16 << 10, [&](const void* Data, size_t Size) {
- const auto* Ptr = (uint8_t*)Data;
- BinScan.insert(BinScan.end(), Ptr, Ptr + Size);
- });
+ std::error_code ScanEc = ScanFile(BinPath, 16 << 10, [&](const void* Data, size_t Size) {
+ const auto* Ptr = (uint8_t*)Data;
+ BinScan.insert(BinScan.end(), Ptr, Ptr + Size);
+ });
+ CHECK(!ScanEc);
CHECK_EQ(BinRead.Data.size(), 1);
CHECK_EQ(BinScan.size(), BinRead.Data[0].GetSize());
}
@@ -3824,9 +3943,9 @@ TEST_CASE("TryCloneFile")
WriteFile(SrcPath, IoBuffer(IoBuffer::Wrap, Content, sizeof(Content)));
CHECK(IsFile(SrcPath));
- bool Cloned = TryCloneFile(SrcPath, DstPath);
+ std::error_code CloneEc = TryCloneFile(SrcPath, DstPath);
- if (Cloned)
+ if (!CloneEc)
{
CHECK(IsFile(DstPath));
CHECK_EQ(FileSizeFromPath(DstPath), sizeof(Content));
@@ -3839,7 +3958,7 @@ TEST_CASE("TryCloneFile")
else
{
// Clone not supported on this filesystem - that's okay, just verify it didn't leave debris
- ZEN_INFO("TryCloneFile not supported on this filesystem, skipping content check");
+ ZEN_INFO("TryCloneFile not supported on this filesystem ({}), skipping content check", CloneEc.message());
}
}
@@ -3853,9 +3972,9 @@ TEST_CASE("TryCloneFile")
WriteFile(DstPath, IoBuffer(IoBuffer::Wrap, OldContent, sizeof(OldContent)));
WriteFile(SrcPath, IoBuffer(IoBuffer::Wrap, NewContent, sizeof(NewContent)));
- bool Cloned = TryCloneFile(SrcPath, DstPath);
+ std::error_code CloneEc = TryCloneFile(SrcPath, DstPath);
- if (Cloned)
+ if (!CloneEc)
{
CHECK_EQ(FileSizeFromPath(DstPath), sizeof(NewContent));
@@ -3870,7 +3989,7 @@ TEST_CASE("TryCloneFile")
std::filesystem::path SrcPath = TestBaseDir / "no_such_file.bin";
std::filesystem::path DstPath = TestBaseDir / "dst_nosrc.bin";
- CHECK_FALSE(TryCloneFile(SrcPath, DstPath));
+ CHECK(TryCloneFile(SrcPath, DstPath));
CHECK_FALSE(IsFile(DstPath));
}
@@ -3892,8 +4011,8 @@ TEST_CASE("CopyFile.Clone")
CopyFileOptions Options;
Options.EnableClone = true;
- bool Success = CopyFile(SrcPath, DstPath, Options);
- CHECK(Success);
+ std::error_code Ec = CopyFile(SrcPath, DstPath, Options);
+ CHECK(!Ec);
CHECK(IsFile(DstPath));
CHECK_EQ(FileSizeFromPath(DstPath), sizeof(Content));
@@ -3908,8 +4027,8 @@ TEST_CASE("CopyFile.Clone")
CopyFileOptions Options;
Options.EnableClone = false;
- bool Success = CopyFile(SrcPath, DstPath, Options);
- CHECK(Success);
+ std::error_code Ec = CopyFile(SrcPath, DstPath, Options);
+ CHECK(!Ec);
CHECK(IsFile(DstPath));
CHECK_EQ(FileSizeFromPath(DstPath), sizeof(Content));
@@ -4108,6 +4227,45 @@ TEST_CASE("filesystem.MakeSafeAbsolutePath")
# endif // ZEN_PLATFORM_WINDOWS
}
+TEST_CASE("ExpandEnvironmentVariables")
+{
+ // No variables - pass-through
+ CHECK_EQ(ExpandEnvironmentVariables("plain/path"), "plain/path");
+ CHECK_EQ(ExpandEnvironmentVariables(""), "");
+
+ // Single percent sign is not a variable reference
+ CHECK_EQ(ExpandEnvironmentVariables("50%"), "50%");
+
+ // Empty variable name (%%) is not expanded
+ CHECK_EQ(ExpandEnvironmentVariables("%%"), "%%");
+
+ // Known variable
+# if ZEN_PLATFORM_WINDOWS
+ // PATH is always set on Windows
+ std::string PathValue = GetEnvVariable("PATH");
+ CHECK(!PathValue.empty());
+ CHECK_EQ(ExpandEnvironmentVariables("%PATH%"), PathValue);
+ CHECK_EQ(ExpandEnvironmentVariables("prefix/%PATH%/suffix"), "prefix/" + PathValue + "/suffix");
+# else
+ std::string HomeValue = GetEnvVariable("HOME");
+ CHECK(!HomeValue.empty());
+ CHECK_EQ(ExpandEnvironmentVariables("%HOME%"), HomeValue);
+ CHECK_EQ(ExpandEnvironmentVariables("prefix/%HOME%/suffix"), "prefix/" + HomeValue + "/suffix");
+# endif
+
+ // Unknown variable is left unexpanded
+ CHECK_EQ(ExpandEnvironmentVariables("%ZEN_UNLIKELY_SET_VAR_12345%"), "%ZEN_UNLIKELY_SET_VAR_12345%");
+
+ // Multiple variables
+# if ZEN_PLATFORM_WINDOWS
+ std::string OSValue = GetEnvVariable("OS");
+ if (!OSValue.empty())
+ {
+ CHECK_EQ(ExpandEnvironmentVariables("%PATH%/%OS%"), PathValue + "/" + OSValue);
+ }
+# endif
+}
+
TEST_SUITE_END();
#endif
diff --git a/src/zencore/include/zencore/basicfile.h b/src/zencore/include/zencore/basicfile.h
index f397370fc..03060cfab 100644
--- a/src/zencore/include/zencore/basicfile.h
+++ b/src/zencore/include/zencore/basicfile.h
@@ -130,6 +130,11 @@ public:
void Create(std::filesystem::path FileName, CbObject Payload, std::error_code& Ec);
void Update(CbObject Payload, std::error_code& Ec);
+ // Probe whether the lock file at FileName is held by a live process.
+ // If AttemptCleanup is true and no live holder is detected, the stale file is deleted.
+ // Returns true iff a live holder exists.
+ static bool IsHeldLive(const std::filesystem::path& FileName, bool AttemptCleanup = false);
+
private:
};
diff --git a/src/zencore/include/zencore/blockingqueue.h b/src/zencore/include/zencore/blockingqueue.h
index b6c93e937..78a59b7fc 100644
--- a/src/zencore/include/zencore/blockingqueue.h
+++ b/src/zencore/include/zencore/blockingqueue.h
@@ -6,9 +6,12 @@
#include <atomic>
#include <condition_variable>
-#include <deque>
#include <mutex>
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <EASTL/deque.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
namespace zen {
template<typename T>
@@ -71,7 +74,7 @@ public:
private:
mutable std::mutex m_Lock;
std::condition_variable m_NewItemSignal;
- std::deque<T> m_Queue;
+ eastl::deque<T> m_Queue;
bool m_CompleteAdding = false;
};
diff --git a/src/zencore/include/zencore/crypto.h b/src/zencore/include/zencore/crypto.h
index 88d156879..a5e23135f 100644
--- a/src/zencore/include/zencore/crypto.h
+++ b/src/zencore/include/zencore/crypto.h
@@ -8,6 +8,7 @@
#include <memory>
#include <optional>
+#include <vector>
namespace zen {
@@ -72,6 +73,21 @@ public:
std::optional<std::string>& Reason);
};
+// Fill Out with cryptographically secure random bytes from the platform RNG.
+// Returns false if the platform RNG failed; on success Out is filled entirely.
+bool SecureRandomBytes(MutableMemoryView Out);
+
+// Wrap Plaintext with per-user OS-protected storage (Windows DPAPI). On success
+// OutProtected holds an opaque blob that only the current OS user on the current
+// machine can UnprotectData. Returns false on platforms without an implementation
+// (currently macOS and Linux) and when the platform call fails; callers should
+// fall back to persisting the plaintext with restrictive file permissions.
+bool TryProtectData(MemoryView Plaintext, std::vector<uint8_t>& OutProtected);
+
+// Inverse of TryProtectData. Returns false if the blob cannot be unwrapped by the
+// current user/machine context.
+bool TryUnprotectData(MemoryView Protected, std::vector<uint8_t>& OutPlaintext);
+
void crypto_forcelink();
} // namespace zen
diff --git a/src/zencore/include/zencore/filesystem.h b/src/zencore/include/zencore/filesystem.h
index 6dc159a83..bce902500 100644
--- a/src/zencore/include/zencore/filesystem.h
+++ b/src/zencore/include/zencore/filesystem.h
@@ -11,6 +11,7 @@
ZEN_THIRD_PARTY_INCLUDES_START
#include <filesystem>
#include <functional>
+#include <optional>
ZEN_THIRD_PARTY_INCLUDES_END
#if ZEN_PLATFORM_WINDOWS
@@ -120,10 +121,10 @@ uint64_t GetModificationTickFromPath(const std::filesystem::path& Filename);
*/
uint64_t GetModificationTickFromHandle(void* NativeHandle, std::error_code& Ec);
-bool TryGetFileProperties(const std::filesystem::path& Path,
- uint64_t& OutSize,
- uint64_t& OutModificationTick,
- uint32_t& OutNativeModeOrAttributes);
+std::error_code TryGetFileProperties(const std::filesystem::path& Path,
+ uint64_t& OutSize,
+ uint64_t& OutModificationTick,
+ uint32_t& OutNativeModeOrAttributes);
/** Move/rename a file, if the files are not on the same drive the function will fail (throws)
*/
@@ -147,7 +148,7 @@ std::filesystem::path GetRunningExecutablePath();
*/
void MaximizeOpenFileCount();
-bool PrepareFileForScatteredWrite(void* FileHandle, uint64_t FinalSize);
+std::error_code PrepareFileForScatteredWrite(void* FileHandle, uint64_t FinalSize);
struct FileContents
{
@@ -174,16 +175,16 @@ FileContents ReadStdIn();
*/
FileContents ReadFile(const std::filesystem::path& Path);
-bool ScanFile(std::filesystem::path Path, uint64_t ChunkSize, std::function<void(const void* Data, size_t Size)>&& ProcessFunc);
-void WriteFile(std::filesystem::path Path, const IoBuffer* const* Data, size_t BufferCount);
-void WriteFile(std::filesystem::path Path, IoBuffer Data);
-void WriteFile(std::filesystem::path Path, CompositeBuffer Data);
-bool MoveToFile(std::filesystem::path Path, IoBuffer Data);
-void ScanFile(void* NativeHandle,
- uint64_t Offset,
- uint64_t Size,
- uint64_t ChunkSize,
- std::function<void(const void* Data, size_t Size)>&& ProcessFunc);
+std::error_code ScanFile(std::filesystem::path Path, uint64_t ChunkSize, std::function<void(const void* Data, size_t Size)>&& ProcessFunc);
+void WriteFile(std::filesystem::path Path, const IoBuffer* const* Data, size_t BufferCount);
+void WriteFile(std::filesystem::path Path, IoBuffer Data);
+void WriteFile(std::filesystem::path Path, CompositeBuffer Data);
+std::error_code MoveToFile(std::filesystem::path Path, IoBuffer Data);
+void ScanFile(void* NativeHandle,
+ uint64_t Offset,
+ uint64_t Size,
+ uint64_t ChunkSize,
+ std::function<void(const void* Data, size_t Size)>&& ProcessFunc);
void WriteFile(void* NativeHandle, const void* Data, uint64_t Size, uint64_t FileOffset, uint64_t ChunkSize, std::error_code& Ec);
void ReadFile(void* NativeHandle, void* Data, uint64_t Size, uint64_t FileOffset, uint64_t ChunkSize, std::error_code& Ec);
@@ -216,7 +217,7 @@ public:
std::unique_ptr<CloneQueryInterface> GetCloneQueryInterface(const std::filesystem::path& TargetDirectory);
-bool TryCloneFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToPath);
+std::error_code TryCloneFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToPath);
struct CopyFileOptions
{
@@ -224,13 +225,9 @@ struct CopyFileOptions
bool MustClone = false;
};
-bool CopyFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToPath, const CopyFileOptions& Options);
-void CopyFile(const std::filesystem::path& FromPath,
- const std::filesystem::path& ToPath,
- const CopyFileOptions& Options,
- std::error_code& OutError);
-void CopyTree(std::filesystem::path FromPath, std::filesystem::path ToPath, const CopyFileOptions& Options);
-bool SupportsBlockRefCounting(std::filesystem::path Path);
+std::error_code CopyFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToPath, const CopyFileOptions& Options);
+void CopyTree(std::filesystem::path FromPath, std::filesystem::path ToPath, const CopyFileOptions& Options);
+bool SupportsBlockRefCounting(std::filesystem::path Path);
void PathToUtf8(const std::filesystem::path& Path, StringBuilderBase& Out);
std::string PathToUtf8(const std::filesystem::path& Path);
@@ -400,6 +397,28 @@ void GetDirectoryContent(const std::filesystem::path& RootDir,
std::string GetEnvVariable(std::string_view VariableName);
+// Expands %VAR% environment variable references in a string.
+// Unknown or empty variables are left unexpanded.
+std::string ExpandEnvironmentVariables(std::string_view Input);
+
+/// Scoped RAII helper to set an environment variable for the duration of its lifetime,
+/// restoring the prior value (or unset state) on destruction. Non-copyable, non-movable.
+class ScopedEnvVar
+{
+public:
+ ScopedEnvVar(std::string_view Name, std::string_view Value);
+ ~ScopedEnvVar();
+
+ ScopedEnvVar(const ScopedEnvVar&) = delete;
+ ScopedEnvVar& operator=(const ScopedEnvVar&) = delete;
+ ScopedEnvVar(ScopedEnvVar&&) = delete;
+ ScopedEnvVar& operator=(ScopedEnvVar&&) = delete;
+
+private:
+ std::string m_Name;
+ std::optional<std::string> m_OldValue; // nullopt = was not set; "" = was set to empty string
+};
+
std::filesystem::path SearchPathForExecutable(std::string_view ExecutableName);
std::error_code RotateFiles(const std::filesystem::path& Filename, std::size_t MaxFiles);
diff --git a/src/zencore/include/zencore/fmtutils.h b/src/zencore/include/zencore/fmtutils.h
index 4ec05f901..f062c4147 100644
--- a/src/zencore/include/zencore/fmtutils.h
+++ b/src/zencore/include/zencore/fmtutils.h
@@ -3,10 +3,7 @@
#pragma once
#include <zencore/filesystem.h>
-#include <zencore/guid.h>
-#include <zencore/iohash.h>
#include <zencore/string.h>
-#include <zencore/uid.h>
ZEN_THIRD_PARTY_INCLUDES_START
#include <fmt/format.h>
@@ -38,63 +35,49 @@ struct fmt::formatter<T> : fmt::formatter<std::string_view>
}
};
-// Custom formatting for some zencore types
+// Generic formatter for any type that is explicitly convertible to std::string_view.
+// This covers NiceNum, NiceBytes, ThousandsNum, StringBuilder, and similar types
+// without needing per-type fmt::formatter specializations.
template<typename T>
-requires DerivedFrom<T, zen::StringBuilderBase>
-struct fmt::formatter<T> : fmt::formatter<std::string_view>
+concept HasStringViewConversion = std::is_class_v<T> && requires(const T& v)
{
- template<typename FormatContext>
- auto format(const zen::StringBuilderBase& a, FormatContext& ctx) const
{
- return fmt::formatter<std::string_view>::format(a.ToView(), ctx);
- }
-};
+ std::string_view(v)
+ } -> std::same_as<std::string_view>;
+} && !HasFreeToString<T> && !std::is_same_v<T, std::string> && !std::is_same_v<T, std::string_view>;
-template<typename T>
-requires DerivedFrom<T, zen::NiceBase>
+template<HasStringViewConversion T>
struct fmt::formatter<T> : fmt::formatter<std::string_view>
{
template<typename FormatContext>
- auto format(const zen::NiceBase& a, FormatContext& ctx) const
+ auto format(const T& Value, FormatContext& ctx) const
{
- return fmt::formatter<std::string_view>::format(std::string_view(a), ctx);
+ return fmt::formatter<std::string_view>::format(std::string_view(Value), ctx);
}
};
-template<>
-struct fmt::formatter<zen::IoHash> : formatter<string_view>
-{
- template<typename FormatContext>
- auto format(const zen::IoHash& Hash, FormatContext& ctx) const
- {
- zen::IoHash::String_t String;
- Hash.ToHexString(String);
- return fmt::formatter<string_view>::format({String, zen::IoHash::StringLength}, ctx);
- }
-};
+// Generic formatter for any type with a ToString(StringBuilderBase&) member function.
+// This covers Guid, IoHash, Oid, and similar types without needing per-type
+// fmt::formatter specializations.
-template<>
-struct fmt::formatter<zen::Oid> : formatter<string_view>
+template<typename T>
+concept HasMemberToStringBuilder = std::is_class_v<T> && requires(const T& v, zen::StringBuilderBase& sb)
{
- template<typename FormatContext>
- auto format(const zen::Oid& Id, FormatContext& ctx) const
{
- zen::StringBuilder<32> String;
- Id.ToString(String);
- return fmt::formatter<string_view>::format({String.c_str(), zen::Oid::StringLength}, ctx);
- }
-};
+ v.ToString(sb)
+ } -> std::same_as<zen::StringBuilderBase&>;
+} && !HasFreeToString<T> && !HasStringViewConversion<T>;
-template<>
-struct fmt::formatter<zen::Guid> : formatter<string_view>
+template<HasMemberToStringBuilder T>
+struct fmt::formatter<T> : fmt::formatter<std::string_view>
{
template<typename FormatContext>
- auto format(const zen::Guid& Id, FormatContext& ctx) const
+ auto format(const T& Value, FormatContext& ctx) const
{
- zen::StringBuilder<48> String;
- Id.ToString(String);
- return fmt::formatter<string_view>::format({String.c_str(), zen::Guid::StringLength}, ctx);
+ zen::ExtendableStringBuilder<64> String;
+ Value.ToString(String);
+ return fmt::formatter<std::string_view>::format(String.ToView(), ctx);
}
};
@@ -107,7 +90,7 @@ struct fmt::formatter<std::filesystem::path> : formatter<string_view>
using namespace std::literals;
zen::ExtendableStringBuilder<128> String;
- String << Path.u8string();
+ zen::PathToUtf8(Path, String);
std::string_view PathView = String.ToView();
if (PathView.starts_with("\\\\?\\"sv))
diff --git a/src/zencore/include/zencore/hashutils.h b/src/zencore/include/zencore/hashutils.h
index 8abfd4b6e..e253d7015 100644
--- a/src/zencore/include/zencore/hashutils.h
+++ b/src/zencore/include/zencore/hashutils.h
@@ -4,6 +4,8 @@
#include <cstddef>
#include <functional>
+#include <string>
+#include <string_view>
#include <type_traits>
namespace zen {
@@ -35,4 +37,21 @@ CombineHashes(const Types&... Args)
return Seed;
}
+/** Transparent string hash for use with std::unordered_map/set.
+ Enables heterogeneous lookup so that a std::string_view can be used to
+ probe a std::string-keyed container without allocating a temporary std::string.
+
+ Usage:
+ std::unordered_map<std::string, V, TransparentStringHash, std::equal_to<>> Map;
+ Map.find(some_string_view); // no allocation
+ */
+struct TransparentStringHash
+{
+ using is_transparent = void;
+
+ size_t operator()(std::string_view Sv) const noexcept { return std::hash<std::string_view>{}(Sv); }
+ size_t operator()(const std::string& S) const noexcept { return std::hash<std::string_view>{}(S); }
+ size_t operator()(const char* S) const noexcept { return std::hash<std::string_view>{}(S); }
+};
+
} // namespace zen
diff --git a/src/zencore/include/zencore/intmath.h b/src/zencore/include/zencore/intmath.h
index 2b59d6f4a..b9f31d57b 100644
--- a/src/zencore/include/zencore/intmath.h
+++ b/src/zencore/include/zencore/intmath.h
@@ -38,6 +38,7 @@
#if ZEN_COMPILER_MSC || ZEN_PLATFORM_WINDOWS
# pragma intrinsic(_BitScanReverse)
# pragma intrinsic(_BitScanReverse64)
+# pragma intrinsic(_umul128)
#else
inline uint8_t
_BitScanReverse(unsigned long* Index, uint32_t Mask)
@@ -205,6 +206,37 @@ Max(auto x, auto y)
//////////////////////////////////////////////////////////////////////////
+// Precomputed reciprocal for fast 64-bit unsigned division by a constant.
+// Given divisor d, stores multiplier m and shift s such that
+// x / d == MulHi64(x, m) >> s for all x in the expected range.
+//
+// Uses the "round-up" method: m = ceil(2^(64+s) / d). The extra shift
+// parameter s is bumped when d is a power of two (where ceil would
+// overflow). For small divisors s == 0 always suffices.
+struct ReciprocalU64
+{
+ uint64_t Mul = 0;
+ uint32_t Shift = 0;
+
+ ReciprocalU64() = default;
+ explicit ReciprocalU64(uint64_t Divisor);
+
+ uint32_t Divide(uint64_t Value) const
+ {
+ if (Mul == 0)
+ {
+ return uint32_t(Value); // Divisor <= 1
+ }
+#if ZEN_PLATFORM_WINDOWS
+ uint64_t Hi;
+ _umul128(Value, Mul, &Hi);
+#else
+ uint64_t Hi = uint64_t(((unsigned __int128)Value * Mul) >> 64);
+#endif
+ return uint32_t(Hi >> Shift);
+ }
+};
+
void intmath_forcelink(); // internal
} // namespace zen
diff --git a/src/zencore/include/zencore/iobuffer.h b/src/zencore/include/zencore/iobuffer.h
index 82c201edd..c6ba90692 100644
--- a/src/zencore/include/zencore/iobuffer.h
+++ b/src/zencore/include/zencore/iobuffer.h
@@ -109,10 +109,11 @@ public:
// Reference counting
- inline uint32_t AddRef() const { return AtomicIncrement(const_cast<IoBufferCore*>(this)->m_RefCount); }
- inline uint32_t Release() const
+ // See zen::RefCounted::AddRef/Release for ordering rationale.
+ inline uint32_t AddRef() const noexcept { return m_RefCount.fetch_add(1, std::memory_order_relaxed) + 1; }
+ inline uint32_t Release() const noexcept
{
- const uint32_t NewRefCount = AtomicDecrement(const_cast<IoBufferCore*>(this)->m_RefCount);
+ const uint32_t NewRefCount = m_RefCount.fetch_sub(1, std::memory_order_acq_rel) - 1;
if (NewRefCount == 0)
{
DeleteThis();
@@ -130,7 +131,7 @@ public:
//
void Materialize() const;
- void DeleteThis() const;
+ void DeleteThis() const noexcept;
void MakeOwned(bool Immutable = true);
inline void EnsureDataValid() const
@@ -228,14 +229,14 @@ public:
return ZenContentType((m_Flags.load(std::memory_order_relaxed) >> kContentTypeShift) & kContentTypeMask);
}
- inline uint32_t GetRefCount() const { return m_RefCount; }
+ inline uint32_t GetRefCount() const noexcept { return m_RefCount.load(std::memory_order_relaxed); }
protected:
- uint32_t m_RefCount = 0;
+ mutable std::atomic<uint32_t> m_RefCount = 0;
mutable std::atomic<uint32_t> m_Flags{0};
mutable const void* m_DataPtr = nullptr;
size_t m_DataBytes = 0;
- RefPtr<const IoBufferCore> m_OuterCore;
+ Ref<const IoBufferCore> m_OuterCore;
enum
{
@@ -413,9 +414,9 @@ public:
private:
// We have a shared "null" buffer core which we share, this is initialized static and never released which will
// cause a memory leak at exit. This does however save millions of memory allocations for null buffers
- static RefPtr<IoBufferCore> NullBufferCore;
+ static Ref<IoBufferCore> NullBufferCore;
- RefPtr<IoBufferCore> m_Core = NullBufferCore;
+ Ref<IoBufferCore> m_Core = NullBufferCore;
IoBuffer(IoBufferCore* Core) : m_Core(Core) {}
diff --git a/src/zencore/include/zencore/iohash.h b/src/zencore/include/zencore/iohash.h
index a619b0053..50c439b70 100644
--- a/src/zencore/include/zencore/iohash.h
+++ b/src/zencore/include/zencore/iohash.h
@@ -54,6 +54,7 @@ struct IoHash
static bool TryParse(std::string_view Str, IoHash& Hash);
const char* ToHexString(char* outString /* 40 characters + NUL terminator */) const;
StringBuilderBase& ToHexString(StringBuilderBase& outBuilder) const;
+ StringBuilderBase& ToString(StringBuilderBase& outBuilder) const { return ToHexString(outBuilder); }
std::string ToHexString() const;
static constexpr int StringLength = 40;
diff --git a/src/zencore/include/zencore/logbase.h b/src/zencore/include/zencore/logbase.h
index ad2ab218d..0c05255ee 100644
--- a/src/zencore/include/zencore/logbase.h
+++ b/src/zencore/include/zencore/logbase.h
@@ -8,7 +8,7 @@
#include <string_view>
namespace zen::logging {
-enum LogLevel : int
+enum LogLevel : int8_t
{
Trace,
Debug,
@@ -21,7 +21,8 @@ enum LogLevel : int
};
LogLevel ParseLogLevelString(std::string_view String);
-std::string_view ToStringView(LogLevel Level);
+std::string_view ToString(LogLevel Level);
+std::string_view ShortToString(LogLevel Level);
void SetLogLevel(LogLevel NewLogLevel);
LogLevel GetLogLevel();
@@ -49,11 +50,16 @@ struct SourceLocation
*/
struct LogPoint
{
- SourceLocation Location;
+ const char* Filename;
+ int Line;
LogLevel Level;
std::string_view FormatString;
+
+ [[nodiscard]] SourceLocation Location() const { return SourceLocation{Filename, Line}; }
};
+static_assert(sizeof(LogPoint) <= 32);
+
class Logger;
/** This is the base class for all loggers
@@ -91,6 +97,7 @@ struct LoggerRef
{
LoggerRef() = default;
explicit LoggerRef(logging::Logger& InLogger);
+ explicit LoggerRef(std::string_view LogCategory);
// This exists so that logging macros can pass LoggerRef or LogCategory
// to ZEN_LOG without needing to know which one it is
@@ -104,6 +111,8 @@ struct LoggerRef
bool ShouldLog(logging::LogLevel Level) const { return m_Logger->ShouldLog(Level); }
void SetLogLevel(logging::LogLevel NewLogLevel) { m_Logger->SetLevel(NewLogLevel); }
logging::LogLevel GetLogLevel() { return m_Logger->GetLevel(); }
+ std::string_view GetLogLevelString() { return logging::ToString(GetLogLevel()); }
+ std::string_view GetShortLogLevelString() { return logging::ShortToString(GetLogLevel()); }
void Flush();
diff --git a/src/zencore/include/zencore/logging.h b/src/zencore/include/zencore/logging.h
index 1608ad523..cf011fb1a 100644
--- a/src/zencore/include/zencore/logging.h
+++ b/src/zencore/include/zencore/logging.h
@@ -131,31 +131,31 @@ using zen::Log;
}
#endif
-#define ZEN_LOG_WITH_LOCATION(InLogger, InLevel, fmtstr, ...) \
- do \
- { \
- using namespace std::literals; \
- static constinit ZEN_LOG_SECTION(".zlog$l") \
- zen::logging::LogPoint LogPoint{zen::logging::SourceLocation{__FILE__, __LINE__}, InLevel, std::string_view(fmtstr)}; \
- zen::LoggerRef Logger = InLogger; \
- ZEN_CHECK_FORMAT_STRING(fmtstr##sv, ##__VA_ARGS__); \
- if (Logger.ShouldLog(InLevel)) \
- { \
- zen::logging::EmitLogMessage(Logger, LogPoint, zen::logging::LogCaptureArguments(__VA_ARGS__)); \
- } \
+#define ZEN_LOG_WITH_LOCATION(InLogger, InLevel, fmtstr, ...) \
+ do \
+ { \
+ using namespace std::literals; \
+ static constinit ZEN_LOG_SECTION(".zlog$l") \
+ zen::logging::LogPoint LogPoint{__FILE__, __LINE__, InLevel, std::string_view(fmtstr)}; \
+ zen::LoggerRef Logger = InLogger; \
+ ZEN_CHECK_FORMAT_STRING(fmtstr##sv, ##__VA_ARGS__); \
+ if (Logger.ShouldLog(InLevel)) \
+ { \
+ zen::logging::EmitLogMessage(Logger, LogPoint, zen::logging::LogCaptureArguments(__VA_ARGS__)); \
+ } \
} while (false);
-#define ZEN_LOG(InLogger, InLevel, fmtstr, ...) \
- do \
- { \
- using namespace std::literals; \
- static constinit ZEN_LOG_SECTION(".zlog$l") zen::logging::LogPoint LogPoint{{}, InLevel, std::string_view(fmtstr)}; \
- zen::LoggerRef Logger = InLogger; \
- ZEN_CHECK_FORMAT_STRING(fmtstr##sv, ##__VA_ARGS__); \
- if (Logger.ShouldLog(InLevel)) \
- { \
- zen::logging::EmitLogMessage(Logger, LogPoint, zen::logging::LogCaptureArguments(__VA_ARGS__)); \
- } \
+#define ZEN_LOG(InLogger, InLevel, fmtstr, ...) \
+ do \
+ { \
+ using namespace std::literals; \
+ static constinit ZEN_LOG_SECTION(".zlog$l") zen::logging::LogPoint LogPoint{0, 0, InLevel, std::string_view(fmtstr)}; \
+ zen::LoggerRef Logger = InLogger; \
+ ZEN_CHECK_FORMAT_STRING(fmtstr##sv, ##__VA_ARGS__); \
+ if (Logger.ShouldLog(InLevel)) \
+ { \
+ zen::logging::EmitLogMessage(Logger, LogPoint, zen::logging::LogCaptureArguments(__VA_ARGS__)); \
+ } \
} while (false);
#define ZEN_DEFINE_LOG_CATEGORY_STATIC(Category, Name) \
@@ -175,13 +175,18 @@ using zen::Log;
#define ZEN_ERROR(fmtstr, ...) ZEN_LOG_WITH_LOCATION(Log(), zen::logging::Err, fmtstr, ##__VA_ARGS__)
#define ZEN_CRITICAL(fmtstr, ...) ZEN_LOG_WITH_LOCATION(Log(), zen::logging::Critical, fmtstr, ##__VA_ARGS__)
-#define ZEN_CONSOLE_LOG(InLevel, fmtstr, ...) \
- do \
- { \
- using namespace std::literals; \
- static constinit ZEN_LOG_SECTION(".zlog$l") zen::logging::LogPoint LogPoint{{}, InLevel, std::string_view(fmtstr)}; \
- ZEN_CHECK_FORMAT_STRING(fmtstr##sv, ##__VA_ARGS__); \
- zen::logging::EmitConsoleLogMessage(LogPoint, zen::logging::LogCaptureArguments(__VA_ARGS__)); \
+// Routes ZEN_INFO / ZEN_WARN / ZEN_DEBUG etc. in the enclosing scope through the given logger expression
+// (a LoggerRef or something convertible, e.g. a member or a context field) instead of the namespace default.
+// Expand at block scope; the resulting local `Log` shadows `zen::Log()` for the rest of the block.
+#define ZEN_SCOPED_LOG(Expr) auto Log = [&]() { return (Expr); }
+
+#define ZEN_CONSOLE_LOG(InLevel, fmtstr, ...) \
+ do \
+ { \
+ using namespace std::literals; \
+ static constinit ZEN_LOG_SECTION(".zlog$l") zen::logging::LogPoint LogPoint{0, 0, InLevel, std::string_view(fmtstr)}; \
+ ZEN_CHECK_FORMAT_STRING(fmtstr##sv, ##__VA_ARGS__); \
+ zen::logging::EmitConsoleLogMessage(LogPoint, zen::logging::LogCaptureArguments(__VA_ARGS__)); \
} while (false)
#define ZEN_CONSOLE(fmtstr, ...) ZEN_CONSOLE_LOG(zen::logging::Info, fmtstr, ##__VA_ARGS__)
diff --git a/src/zencore/include/zencore/logging/broadcastsink.h b/src/zencore/include/zencore/logging/broadcastsink.h
index c2709d87c..474662888 100644
--- a/src/zencore/include/zencore/logging/broadcastsink.h
+++ b/src/zencore/include/zencore/logging/broadcastsink.h
@@ -17,7 +17,7 @@ namespace zen::logging {
/// sink is immediately visible to all of them. This is the recommended way
/// to manage "default" sinks that should be active on most loggers.
///
-/// Each child sink owns its own Formatter — BroadcastSink::SetFormatter() is
+/// Each child sink owns its own Formatter - BroadcastSink::SetFormatter() is
/// intentionally a no-op so that per-sink formatting is not accidentally
/// overwritten by registry-wide formatter changes.
class BroadcastSink : public Sink
@@ -63,7 +63,7 @@ public:
}
}
- /// No-op — child sinks manage their own formatters.
+ /// No-op - child sinks manage their own formatters.
void SetFormatter(std::unique_ptr<Formatter> /*InFormatter*/) override {}
void AddSink(SinkPtr InSink)
diff --git a/src/zencore/include/zencore/logging/helpers.h b/src/zencore/include/zencore/logging/helpers.h
index 765aa59e3..77964b05d 100644
--- a/src/zencore/include/zencore/logging/helpers.h
+++ b/src/zencore/include/zencore/logging/helpers.h
@@ -116,7 +116,7 @@ ShortFilename(const char* Path)
inline std::string_view
LevelToShortString(LogLevel Level)
{
- return ToStringView(Level);
+ return ShortToString(Level);
}
inline std::string_view
diff --git a/src/zencore/include/zencore/logging/logmsg.h b/src/zencore/include/zencore/logging/logmsg.h
index 4a777c71e..644af2730 100644
--- a/src/zencore/include/zencore/logging/logmsg.h
+++ b/src/zencore/include/zencore/logging/logmsg.h
@@ -7,49 +7,50 @@
#include <chrono>
#include <string_view>
+namespace zen {
+int GetCurrentThreadId();
+}
+
namespace zen::logging {
using LogClock = std::chrono::system_clock;
+/**
+ * This represents a single log event, with all the data needed to format and
+ * emit the final log message.
+ *
+ * LogMessage is what gets passed to Sinks, and it's what contains all the
+ * contextual information about the log event.
+ */
+
struct LogMessage
{
- LogMessage() = default;
-
LogMessage(const LogPoint& InPoint, std::string_view InLoggerName, std::string_view InPayload)
: m_LoggerName(InLoggerName)
- , m_Level(InPoint.Level)
, m_Time(LogClock::now())
- , m_Source(InPoint.Location)
+ , m_ThreadId(zen::GetCurrentThreadId())
, m_Payload(InPayload)
, m_Point(&InPoint)
{
}
- std::string_view GetPayload() const { return m_Payload; }
- int GetThreadId() const { return m_ThreadId; }
- LogClock::time_point GetTime() const { return m_Time; }
- LogLevel GetLevel() const { return m_Level; }
- std::string_view GetLoggerName() const { return m_LoggerName; }
- const SourceLocation& GetSource() const { return m_Source; }
- const LogPoint& GetLogPoint() const { return *m_Point; }
+ [[nodiscard]] std::string_view GetPayload() const { return m_Payload; }
+ [[nodiscard]] int GetThreadId() const { return m_ThreadId; }
+ [[nodiscard]] LogClock::time_point GetTime() const { return m_Time; }
+ [[nodiscard]] LogLevel GetLevel() const { return m_Point->Level; }
+ [[nodiscard]] std::string_view GetLoggerName() const { return m_LoggerName; }
+ [[nodiscard]] SourceLocation GetSource() const { return m_Point->Location(); }
+ [[nodiscard]] const LogPoint& GetLogPoint() const { return *m_Point; }
void SetThreadId(int InThreadId) { m_ThreadId = InThreadId; }
- void SetPayload(std::string_view InPayload) { m_Payload = InPayload; }
- void SetLoggerName(std::string_view InName) { m_LoggerName = InName; }
- void SetLevel(LogLevel InLevel) { m_Level = InLevel; }
void SetTime(LogClock::time_point InTime) { m_Time = InTime; }
- void SetSource(const SourceLocation& InSource) { m_Source = InSource; }
private:
- static constexpr LogPoint s_DefaultPoint{{}, Off, {}};
-
- std::string_view m_LoggerName;
- LogLevel m_Level = Off;
- std::chrono::system_clock::time_point m_Time;
- SourceLocation m_Source;
- std::string_view m_Payload;
- const LogPoint* m_Point = &s_DefaultPoint;
- int m_ThreadId = 0;
+ std::string_view m_LoggerName;
+ LogClock::time_point m_Time;
+ int m_ThreadId;
+ std::string_view m_Payload;
+ const LogPoint* m_Point;
};
} // namespace zen::logging
diff --git a/src/zencore/include/zencore/logging/tracelog.h b/src/zencore/include/zencore/logging/tracelog.h
new file mode 100644
index 000000000..43ed2cc93
--- /dev/null
+++ b/src/zencore/include/zencore/logging/tracelog.h
@@ -0,0 +1,63 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zencore/zencore.h>
+
+#if ZEN_WITH_TRACE
+
+ZEN_THIRD_PARTY_INCLUDES_START
+# include <fmt/base.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+# include <cstdint>
+# include <string>
+# include <string_view>
+# include <vector>
+
+namespace zen::logging {
+
+struct LogPoint;
+class Logger;
+
+/// Forward a zencore log event to the UE trace runtime on the ZenLogChannel.
+///
+/// Called from inside Logger::Log while the typed format argument pack is
+/// still available, so we can encode individual arguments into the trace
+/// payload instead of shipping a pre-rendered string. The function is a
+/// no-op when the channel is not active.
+///
+/// The wire format uses a zen-specific event schema ("ZenLog.Category",
+/// "ZenLog.MessageSpec", "ZenLog.Message") rather than UE's "Logging.*"
+/// events, because our format strings use fmt's `{}` placeholders that
+/// UE Insights' printf-based log analyzer doesn't understand. Insights
+/// can be taught about the ZenLog schema later; in the meantime our own
+/// trace analyzer (src/zen/trace/trace_model.cpp) decodes them.
+void TraceLogTyped(const Logger& Logger, const LogPoint& Point, fmt::format_args Args);
+
+/// Encode an `fmt::format_args` pack into a ZenLog FormatArgs blob.
+///
+/// The output layout is `[count:uint8][descriptors:count bytes][payload]`,
+/// matching what `TraceLogTyped` emits in the `ZenLog.Message.FormatArgs`
+/// field. Exposed primarily for tests that exercise the encode/decode
+/// round-trip, but also useful for any future consumer that wants to
+/// serialize args without routing through the trace channel.
+///
+/// At most 255 args are encoded; additional args are silently dropped so
+/// the one-byte count field cannot overflow.
+void EncodeLogArgs(fmt::format_args Args, std::vector<uint8_t>& Out);
+
+/// Render `Format` against a ZenLog FormatArgs blob.
+///
+/// Args are decoded back to typed values (bool / int64 / uint64 / float /
+/// double / string_view / void*) and handed to `fmt::vformat` so the full
+/// fmt format-spec grammar works end-to-end (width/precision, type specs,
+/// nested widths, ...). If formatting throws - malformed `Format`,
+/// arg-count or type mismatch, spec incompatible with the decoded arg
+/// type, etc. - the error and the original template are returned as the
+/// rendered string rather than propagating the exception.
+std::string FormatLogArgs(std::string_view Format, const uint8_t* Data, size_t Size);
+
+} // namespace zen::logging
+
+#endif // ZEN_WITH_TRACE
diff --git a/src/zencore/include/zencore/logging/tracesink.h b/src/zencore/include/zencore/logging/tracesink.h
deleted file mode 100644
index 785c51e10..000000000
--- a/src/zencore/include/zencore/logging/tracesink.h
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright Epic Games, Inc. All Rights Reserved.
-
-#pragma once
-
-#include <zencore/logging/sink.h>
-
-namespace zen::logging {
-
-#if ZEN_WITH_TRACE
-
-/**
- * A logging sink that forwards log messages to the trace system.
- *
- * Work-in-progress, not fully implemented.
- */
-
-class TraceSink : public Sink
-{
-public:
- void Log(const LogMessage& Msg) override;
- void Flush() override;
- void SetFormatter(std::unique_ptr<Formatter> InFormatter) override;
-};
-
-#endif
-
-} // namespace zen::logging
diff --git a/src/zencore/include/zencore/mpscqueue.h b/src/zencore/include/zencore/mpscqueue.h
index d97c433fd..38a0bc14f 100644
--- a/src/zencore/include/zencore/mpscqueue.h
+++ b/src/zencore/include/zencore/mpscqueue.h
@@ -11,7 +11,7 @@
using std::hardware_constructive_interference_size;
using std::hardware_destructive_interference_size;
#else
-// 64 bytes on x86-64 │ L1_CACHE_BYTES │ L1_CACHE_SHIFT │ __cacheline_aligned │ ...
+// 64 bytes on x86-64 | L1_CACHE_BYTES | L1_CACHE_SHIFT | __cacheline_aligned | ...
constexpr std::size_t hardware_constructive_interference_size = 64;
constexpr std::size_t hardware_destructive_interference_size = 64;
#endif
diff --git a/src/zencore/include/zencore/process.h b/src/zencore/include/zencore/process.h
index 5ae7fad68..4f55d9a0f 100644
--- a/src/zencore/include/zencore/process.h
+++ b/src/zencore/include/zencore/process.h
@@ -6,6 +6,7 @@
#include <zencore/zencore.h>
#include <filesystem>
+#include <span>
#include <string>
#include <utility>
#include <vector>
@@ -34,7 +35,7 @@ public:
/// Throws std::system_error on failure.
explicit ProcessHandle(int Pid);
- /// Construct from an existing native process handle. Takes ownership —
+ /// Construct from an existing native process handle. Takes ownership -
/// the caller must not close the handle afterwards. Windows only.
#if ZEN_PLATFORM_WINDOWS
explicit ProcessHandle(void* NativeHandle);
@@ -56,7 +57,7 @@ public:
/// Same as Initialize(int) but reports errors via @p OutEc instead of throwing.
void Initialize(int Pid, std::error_code& OutEc);
- /// Initialize from an existing native process handle. Takes ownership —
+ /// Initialize from an existing native process handle. Takes ownership -
/// the caller must not close the handle afterwards. Windows only.
#if ZEN_PLATFORM_WINDOWS
void Initialize(void* ProcessHandle);
@@ -160,6 +161,42 @@ struct StdoutPipeHandles
// The write end is inheritable; the read end is not.
bool CreateStdoutPipe(StdoutPipeHandles& OutPipe);
+// Platform-agnostic RAII pipe handles for feeding data into a child's stdin.
+// The destructor closes any open handles/fds automatically.
+struct StdinPipeHandles
+{
+ StdinPipeHandles() = default;
+ ~StdinPipeHandles();
+
+ StdinPipeHandles(const StdinPipeHandles&) = delete;
+ StdinPipeHandles& operator=(const StdinPipeHandles&) = delete;
+
+ StdinPipeHandles(StdinPipeHandles&& Other) noexcept;
+ StdinPipeHandles& operator=(StdinPipeHandles&& Other) noexcept;
+
+ // Close only the read end (call after child is launched so parent doesn't hold it open;
+ // without this the child sees EOF only after the parent closes too).
+ void CloseReadEnd();
+
+ // Close only the write end. Signals EOF to the child once the parent is done writing.
+ void CloseWriteEnd();
+
+ // Close both ends of the pipe.
+ void Close();
+
+#if ZEN_PLATFORM_WINDOWS
+ void* ReadHandle = nullptr; // HANDLE for reading (child side)
+ void* WriteHandle = nullptr; // HANDLE for writing (parent side)
+#else
+ int ReadFd = -1;
+ int WriteFd = -1;
+#endif
+};
+
+// Create a pipe suitable for feeding data into child process stdin.
+// The read end is inheritable; the write end is not.
+bool CreateStdinPipe(StdinPipeHandles& OutPipe);
+
struct CreateProcOptions
{
enum
@@ -174,13 +211,18 @@ struct CreateProcOptions
// allocated and no conhost.exe is spawned. Stdout/stderr still work when redirected
// via pipes. Prefer this for headless worker processes.
Flag_NoConsole = 1 << 3,
- // Create the child in a new process group (CREATE_NEW_PROCESS_GROUP on Windows).
- // Allows sending CTRL_BREAK_EVENT to the child group without affecting the parent.
- Flag_Windows_NewProcessGroup = 1 << 4,
+ // Spawn the child as a new process group leader (its pgid = its own pid).
+ // On Windows: CREATE_NEW_PROCESS_GROUP, enables CTRL_BREAK_EVENT targeting.
+ // On POSIX: child calls setpgid(0,0) / posix_spawn with POSIX_SPAWN_SETPGROUP+pgid=0.
+ // Mutually exclusive with ProcessGroupId > 0.
+ Flag_NewProcessGroup = 1 << 4,
// Allocate a hidden console for the child (CREATE_NO_WINDOW on Windows). Unlike
// Flag_NoConsole the child still gets a console (and a conhost.exe) but no visible
// window. Use this when the child needs a console for stdio but should not show a window.
Flag_NoWindow = 1 << 5,
+ // Launch the child at below-normal scheduling priority.
+ // On Windows: BELOW_NORMAL_PRIORITY_CLASS. On POSIX: nice(5).
+ Flag_BelowNormalPriority = 1 << 6,
};
const std::filesystem::path* WorkingDirectory = nullptr;
@@ -188,18 +230,19 @@ struct CreateProcOptions
std::filesystem::path StdoutFile;
StdoutPipeHandles* StdoutPipe = nullptr; // Mutually exclusive with StdoutFile. Parent reads from ReadHandle after launch.
StdoutPipeHandles* StderrPipe = nullptr; // Optional separate pipe for stderr. When null, stderr shares StdoutPipe.
+ StdinPipeHandles* StdinPipe = nullptr; // Optional pipe feeding child stdin. Parent writes to WriteHandle after launch.
/// Additional environment variables for the child process. These are merged
- /// with the parent's environment — existing variables are inherited, and
+ /// with the parent's environment - existing variables are inherited, and
/// entries here override or add to them.
std::vector<std::pair<std::string, std::string>> Environment;
#if ZEN_PLATFORM_WINDOWS
JobObject* AssignToJob = nullptr; // When set, the process is created suspended, assigned to the job, then resumed
#else
- /// POSIX process group id. When > 0, the child is placed into this process
- /// group via setpgid() before exec. Use the pid of the first child as the
- /// pgid to create a group, then pass the same pgid for subsequent children.
+ /// When > 0, child joins this existing process group. Mutually exclusive with
+ /// Flag_NewProcessGroup; use that flag on the first spawn to create the group,
+ /// then pass the resulting pid here for subsequent spawns to join it.
int ProcessGroupId = 0;
#endif
};
@@ -273,7 +316,31 @@ int GetProcessId(CreateProcResult ProcId);
std::filesystem::path GetProcessExecutablePath(int Pid, std::error_code& OutEc);
std::string GetProcessCommandLine(int Pid, std::error_code& OutEc);
-std::error_code FindProcess(const std::filesystem::path& ExecutableImage, ProcessHandle& OutHandle, bool IncludeSelf = true);
+// If ExecutableImage has a parent path, full-path equality is compared against the
+// process executable path returned by GetProcessExecutablePath; caller must pass an
+// absolute path (the function does not canonicalize). If ExecutableImage is a bare
+// filename only the process basename is matched.
+std::error_code FindProcess(const std::filesystem::path& ExecutableImage, ProcessHandle& OutHandle, bool IncludeSelf = true);
+
+/// Return the current process's raw command line as UTF-8.
+///
+/// On Windows, reads GetCommandLineW() from the PEB and converts to UTF-8.
+/// This is the true Unicode command line, unaffected by the ANSI downgrade that
+/// the CRT applies to `char** argv` passed to `main`.
+///
+/// On POSIX, there is no kernel-preserved form distinct from the process argv,
+/// so this returns an empty string. Callers should fall back to joining argv
+/// via BuildCommandLine().
+std::string GetRawCommandLine();
+
+/// Build a single command-line string from an argv array.
+///
+/// Any argument containing whitespace or a double-quote is wrapped in "..."
+/// with embedded " escaped as \". Arguments without special characters are
+/// emitted verbatim. The output round-trips through CommandLineToArgvW on
+/// Windows and through BuildArgV (the internal POSIX splitter) back to the
+/// original argv strings.
+std::string BuildCommandLine(std::span<const std::string> Argv);
/** Wait for all threads in the current process to exit (except the calling thread)
*
diff --git a/src/zencore/include/zencore/sentryintegration.h b/src/zencore/include/zencore/sentryintegration.h
index 27e5a8a82..532db19a9 100644
--- a/src/zencore/include/zencore/sentryintegration.h
+++ b/src/zencore/include/zencore/sentryintegration.h
@@ -13,7 +13,10 @@
# include <zencore/logging/logger.h>
+# include <filesystem>
# include <memory>
+# include <string>
+# include <vector>
namespace sentry {
@@ -31,12 +34,12 @@ public:
struct Config
{
- std::string DatabasePath;
- std::string AttachmentsPath;
- std::string Dsn;
- std::string Environment;
- bool AllowPII = false;
- bool Debug = false;
+ std::string DatabasePath;
+ std::vector<std::filesystem::path> AttachmentPaths;
+ std::string Dsn;
+ std::string Environment;
+ bool AllowPII = false;
+ bool Debug = false;
};
void Initialize(const Config& Conf, const std::string& CommandLine);
diff --git a/src/zencore/include/zencore/sharedbuffer.h b/src/zencore/include/zencore/sharedbuffer.h
index 3d4c19282..3183c7c0c 100644
--- a/src/zencore/include/zencore/sharedbuffer.h
+++ b/src/zencore/include/zencore/sharedbuffer.h
@@ -65,7 +65,7 @@ public:
private:
// This may be null, for a default constructed UniqueBuffer only
- RefPtr<IoBufferCore> m_Buffer;
+ Ref<IoBufferCore> m_Buffer;
friend class SharedBuffer;
};
@@ -81,7 +81,7 @@ public:
inline explicit SharedBuffer(IoBufferCore* Owner) : m_Buffer(Owner) {}
explicit SharedBuffer(IoBuffer&& Buffer) : m_Buffer(std::move(Buffer.m_Core)) {}
explicit SharedBuffer(const IoBuffer& Buffer) : m_Buffer(Buffer.m_Core) {}
- explicit SharedBuffer(RefPtr<IoBufferCore>&& Owner) : m_Buffer(std::move(Owner)) {}
+ explicit SharedBuffer(Ref<IoBufferCore>&& Owner) : m_Buffer(std::move(Owner)) {}
[[nodiscard]] const void* GetData() const
{
@@ -143,7 +143,7 @@ public:
/** Returns true if this points to a buffer owner. */
[[nodiscard]] inline explicit operator bool() const { return !IsNull(); }
- [[nodiscard]] inline IoBuffer AsIoBuffer() const { return IoBuffer(m_Buffer); }
+ [[nodiscard]] inline IoBuffer AsIoBuffer() const { return IoBuffer(m_Buffer.Get()); }
SharedBuffer& operator=(UniqueBuffer&& Rhs)
{
@@ -171,7 +171,7 @@ public:
[[nodiscard]] static SharedBuffer Clone(MemoryView View);
private:
- RefPtr<IoBufferCore> m_Buffer;
+ Ref<IoBufferCore> m_Buffer;
};
void sharedbuffer_forcelink();
diff --git a/src/zencore/include/zencore/string.h b/src/zencore/include/zencore/string.h
index 60293a313..a1c7a3914 100644
--- a/src/zencore/include/zencore/string.h
+++ b/src/zencore/include/zencore/string.h
@@ -402,6 +402,12 @@ public:
inline std::string_view ToView() const { return std::string_view(m_Base, m_CurPos - m_Base); }
inline std::string ToString() const { return std::string{Data(), Size()}; }
+ /// Append a zero-padded decimal integer. MinWidth is the minimum number of digits (zero-padded on the left).
+ void AppendPaddedInt(int64_t Value, int MinWidth);
+
+ /// Append a single character repeated Count times.
+ void AppendFill(char C, size_t Count);
+
inline void AppendCodepoint(uint32_t cp)
{
if (cp < 0x80) // one octet
@@ -435,6 +441,24 @@ public:
}
};
+/// Output iterator adapter for StringBuilderBase, enabling direct use with fmt::format_to / fmt::format_to_n.
+class StringBuilderAppender
+{
+ StringBuilderBase* m_Builder;
+
+public:
+ explicit StringBuilderAppender(StringBuilderBase& Builder) : m_Builder(&Builder) {}
+
+ StringBuilderAppender& operator=(char C)
+ {
+ m_Builder->Append(C);
+ return *this;
+ }
+ StringBuilderAppender& operator*() { return *this; }
+ StringBuilderAppender& operator++() { return *this; }
+ StringBuilderAppender operator++(int) { return *this; }
+};
+
template<size_t N>
class StringBuilder : public StringBuilderBase
{
@@ -609,6 +633,17 @@ ParseHexBytes(std::string_view InputString, uint8_t* OutPtr)
return ParseHexBytes(InputString.data(), InputString.size(), OutPtr);
}
+/** Parse hex string into a byte buffer, validating that the hex string is exactly ExpectedByteCount * 2 characters. */
+inline bool
+ParseHexBytes(std::string_view InputString, uint8_t* OutPtr, size_t ExpectedByteCount)
+{
+ if (InputString.size() != ExpectedByteCount * 2)
+ {
+ return false;
+ }
+ return ParseHexBytes(InputString.data(), InputString.size(), OutPtr);
+}
+
inline void
ToHexBytes(const uint8_t* InputData, size_t ByteCount, char* OutString)
{
@@ -722,6 +757,32 @@ struct NiceNum : public NiceBase
inline NiceNum(uint64_t Num) { NiceNumToBuffer(Num, m_Buffer); }
};
+size_t ThousandsToBuffer(uint64_t Num, std::span<char> Buffer);
+
+/// Integer formatted with comma thousands separators (e.g. "1,234,567")
+struct ThousandsNum
+{
+ inline ThousandsNum(UnsignedIntegral auto Number) { ThousandsToBuffer(uint64_t(Number), m_Buffer); }
+ inline ThousandsNum(SignedIntegral auto Number)
+ {
+ if (Number < 0)
+ {
+ m_Buffer[0] = '-';
+ ThousandsToBuffer(uint64_t(-Number), std::span<char>(m_Buffer + 1, sizeof(m_Buffer) - 1));
+ }
+ else
+ {
+ ThousandsToBuffer(uint64_t(Number), m_Buffer);
+ }
+ }
+
+ inline const char* c_str() const { return m_Buffer; }
+ inline operator std::string_view() const { return std::string_view(m_Buffer); }
+
+private:
+ char m_Buffer[28]; // max uint64: "18,446,744,073,709,551,615" (26) + NUL + sign
+};
+
struct NiceBytes : public NiceBase
{
inline NiceBytes(uint64_t Num) { NiceBytesToBuffer(Num, m_Buffer); }
@@ -1288,6 +1349,70 @@ std::string HideSensitiveString(std::string_view String);
//////////////////////////////////////////////////////////////////////////
+/// Owns a heap-allocated, null-terminated string buffer.
+/// Lightweight alternative to std::string when only immutable
+/// storage and string_view access are needed.
+///
+/// The buffer layout is [length-prefix][chars...][NUL]. The first
+/// byte stores the string length for strings shorter than 255 bytes,
+/// avoiding strlen() on every access. A sentinel value of 0xFF
+/// indicates a longer string; Size() then returns 255 + strlen()
+/// starting at the 256th character, skipping the known prefix.
+class CompactString
+{
+public:
+ CompactString() = default;
+
+ explicit CompactString(std::string_view Str) : m_Data(new char[1 + Str.size() + 1])
+ {
+ m_Data[0] = static_cast<char>(Str.size() < LengthFallbackSentinel ? Str.size() : LengthFallbackSentinel);
+ memcpy(m_Data + 1, Str.data(), Str.size());
+ m_Data[1 + Str.size()] = '\0';
+ }
+
+ ~CompactString() { delete[] m_Data; }
+
+ CompactString(const CompactString&) = delete;
+ CompactString& operator=(const CompactString&) = delete;
+
+ CompactString(CompactString&& Other) noexcept : m_Data(Other.m_Data) { Other.m_Data = nullptr; }
+
+ CompactString& operator=(CompactString&& Other) noexcept
+ {
+ if (this != &Other)
+ {
+ delete[] m_Data;
+ m_Data = Other.m_Data;
+ Other.m_Data = nullptr;
+ }
+ return *this;
+ }
+
+ const char* c_str() const { return m_Data ? m_Data + 1 : ""; }
+
+ size_t Size() const
+ {
+ if (!m_Data)
+ {
+ return 0;
+ }
+ uint8_t Prefix = static_cast<uint8_t>(m_Data[0]);
+ return Prefix < LengthFallbackSentinel ? Prefix : LengthFallbackSentinel + strlen(m_Data + 1 + LengthFallbackSentinel);
+ }
+
+ std::string_view ToView() const { return {c_str(), Size()}; }
+ bool IsEmpty() const { return m_Data == nullptr || m_Data[1] == '\0'; }
+
+ operator std::string_view() const { return ToView(); }
+
+private:
+ static constexpr uint8_t LengthFallbackSentinel = 0xFF;
+
+ char* m_Data = nullptr;
+};
+
+//////////////////////////////////////////////////////////////////////////
+
void string_forcelink(); // internal
} // namespace zen
diff --git a/src/zencore/include/zencore/system.h b/src/zencore/include/zencore/system.h
index 52dafc18b..efc9bb6d2 100644
--- a/src/zencore/include/zencore/system.h
+++ b/src/zencore/include/zencore/system.h
@@ -46,6 +46,11 @@ struct ExtendedSystemMetrics : SystemMetrics
SystemMetrics GetSystemMetrics();
+/// Lightweight query that only refreshes fields that change at runtime
+/// (available memory, uptime). Topology fields (CPU/core counts, total memory)
+/// are left at their default values and must be filled from a cached snapshot.
+void RefreshDynamicSystemMetrics(SystemMetrics& InOutMetrics);
+
void SetCpuCountForReporting(int FakeCpuCount);
SystemMetrics GetSystemMetricsForReporting();
diff --git a/src/zencore/include/zencore/testutils.h b/src/zencore/include/zencore/testutils.h
index 2a789d18f..68461deb2 100644
--- a/src/zencore/include/zencore/testutils.h
+++ b/src/zencore/include/zencore/testutils.h
@@ -62,24 +62,24 @@ struct TrueType
namespace utf8test {
// 2-byte UTF-8 (Latin extended)
- static constexpr const char kLatin[] = u8"café_résumé";
- static constexpr const wchar_t kLatinW[] = L"café_résumé";
+ static constexpr const char kLatin[] = u8"caf\xC3\xA9_r\xC3\xA9sum\xC3\xA9";
+ static constexpr const wchar_t kLatinW[] = L"caf\u00E9_r\u00E9sum\u00E9";
// 2-byte UTF-8 (Cyrillic)
- static constexpr const char kCyrillic[] = u8"данные";
- static constexpr const wchar_t kCyrillicW[] = L"данные";
+ static constexpr const char kCyrillic[] = u8"\xD0\xB4\xD0\xB0\xD0\xBD\xD0\xBD\xD1\x8B\xD0\xB5";
+ static constexpr const wchar_t kCyrillicW[] = L"\u0434\u0430\u043D\u043D\u044B\u0435";
// 3-byte UTF-8 (CJK)
- static constexpr const char kCJK[] = u8"日本語";
- static constexpr const wchar_t kCJKW[] = L"日本語";
+ static constexpr const char kCJK[] = u8"\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E";
+ static constexpr const wchar_t kCJKW[] = L"\u65E5\u672C\u8A9E";
// Mixed scripts
- static constexpr const char kMixed[] = u8"zen_éд日";
- static constexpr const wchar_t kMixedW[] = L"zen_éд日";
+ static constexpr const char kMixed[] = u8"zen_\xC3\xA9\xD0\xB4\xE6\x97\xA5";
+ static constexpr const wchar_t kMixedW[] = L"zen_\u00E9\u0434\u65E5";
- // 4-byte UTF-8 (supplementary plane) — string tests only, NOT filesystem
- static constexpr const char kEmoji[] = u8"📦";
- static constexpr const wchar_t kEmojiW[] = L"📦";
+ // 4-byte UTF-8 (supplementary plane) - string tests only, NOT filesystem
+ static constexpr const char kEmoji[] = u8"\xF0\x9F\x93\xA6";
+ static constexpr const wchar_t kEmojiW[] = L"\U0001F4E6";
// BMP-only test strings suitable for filesystem use
static constexpr const char* kFilenameSafe[] = {kLatin, kCyrillic, kCJK, kMixed};
diff --git a/src/zencore/include/zencore/thread.h b/src/zencore/include/zencore/thread.h
index 56ce5904b..0f7733df5 100644
--- a/src/zencore/include/zencore/thread.h
+++ b/src/zencore/include/zencore/thread.h
@@ -14,7 +14,7 @@
namespace zen {
-void SetCurrentThreadName(std::string_view ThreadName);
+void SetCurrentThreadName(std::string_view ThreadName, int32_t SortHint = 0);
/**
* Reader-writer lock
diff --git a/src/zencore/include/zencore/trace.h b/src/zencore/include/zencore/trace.h
index d17e018ea..319a1ecf3 100644
--- a/src/zencore/include/zencore/trace.h
+++ b/src/zencore/include/zencore/trace.h
@@ -19,6 +19,8 @@ ZEN_THIRD_PARTY_INCLUDES_END
#define ZEN_TRACE_CPU(x) TRACE_CPU_SCOPE(x)
#define ZEN_TRACE_CPU_FLUSH(x) TRACE_CPU_SCOPE(x, trace::CpuScopeFlags::CpuFlush)
+#define ZEN_TRACE_CONCAT_IMPL(a, b) a##b
+#define ZEN_TRACE_CONCAT(a, b) ZEN_TRACE_CONCAT_IMPL(a, b)
namespace zen {
@@ -37,12 +39,59 @@ bool TraceStop();
bool GetTraceOptionsFromCommandline(TraceOptions& OutOptions);
void TraceConfigure(const TraceOptions& Options);
+uint64_t TraceBeginRegion(std::string_view RegionName, std::string_view Category = {});
+void TraceEndRegion(uint64_t RegionId);
+
+class ScopedTraceRegion
+{
+public:
+ ScopedTraceRegion(std::string_view RegionName, std::string_view Category = {});
+ ~ScopedTraceRegion();
+
+ ScopedTraceRegion(const ScopedTraceRegion&) = delete;
+ ScopedTraceRegion& operator=(const ScopedTraceRegion&) = delete;
+
+private:
+ uint64_t m_RegionId = 0;
+};
+
}
+#define ZEN_TRACE_REGION(name) ::zen::ScopedTraceRegion ZEN_TRACE_CONCAT(__ScopedTraceRegion_, __LINE__)(name)
+#define ZEN_TRACE_REGION_CAT(name, category) ::zen::ScopedTraceRegion ZEN_TRACE_CONCAT(__ScopedTraceRegion_, __LINE__)(name, category)
+
#else
+namespace zen {
+
+struct TraceOptions
+{
+ std::string Host;
+ std::string File;
+ std::string Channels;
+};
+
+inline void TraceInit(std::string_view) {}
+inline void TraceShutdown() {}
+inline bool IsTracing() { return false; }
+inline bool TraceStop() { return false; }
+inline bool GetTraceOptionsFromCommandline(TraceOptions&) { return false; }
+inline void TraceConfigure(const TraceOptions&) {}
+inline uint64_t TraceBeginRegion(std::string_view, std::string_view = {}) { return 0; }
+inline void TraceEndRegion(uint64_t) {}
+
+class ScopedTraceRegion
+{
+public:
+ ScopedTraceRegion(std::string_view, std::string_view = {}) {}
+};
+
+}
+
#define ZEN_TRACE_CPU(x)
#define ZEN_TRACE_CPU_FLUSH(x)
+#define ZEN_TRACE_REGION(name)
+#define ZEN_TRACE_REGION_CAT(name, category)
#endif // ZEN_WITH_TRACE
diff --git a/src/zencore/include/zencore/zencore.h b/src/zencore/include/zencore/zencore.h
index a31950b0b..57c7e20fa 100644
--- a/src/zencore/include/zencore/zencore.h
+++ b/src/zencore/include/zencore/zencore.h
@@ -94,7 +94,7 @@ protected:
// With no extra args: ZEN_ASSERT_MSG_("expr") -> "expr"
// With a message arg: ZEN_ASSERT_MSG_("expr", "msg") -> "expr" ": " "msg"
// With fmt-style args: ZEN_ASSERT_MSG_("expr", "msg", args...) -> "expr" ": " "msg"
-// The extra fmt args are silently discarded here — use ZEN_ASSERT_FORMAT for those.
+// The extra fmt args are silently discarded here - use ZEN_ASSERT_FORMAT for those.
#define ZEN_ASSERT_MSG_SELECT_(_1, _2, N, ...) N
#define ZEN_ASSERT_MSG_1_(expr) expr
#define ZEN_ASSERT_MSG_2_(expr, msg, ...) expr ": " msg
diff --git a/src/zencore/intmath.cpp b/src/zencore/intmath.cpp
index fedf76edc..b460b5b78 100644
--- a/src/zencore/intmath.cpp
+++ b/src/zencore/intmath.cpp
@@ -7,6 +7,43 @@
namespace zen {
+ReciprocalU64::ReciprocalU64(uint64_t Divisor)
+{
+ if (Divisor <= 1)
+ {
+ Mul = 0; // Sentinel — Divide() returns Value directly.
+ Shift = 0;
+ return;
+ }
+
+ // m = ceil(2^(64+s) / d). Start with s = 0; bump s only if
+ // the quotient doesn't fit in 64 bits (happens when d is a
+ // power of two, since 2^64 / 2^k = 2^(64-k) exactly and the
+ // +1 for ceil can overflow to zero).
+ for (uint32_t S = 0; S < 64; ++S)
+ {
+#if ZEN_PLATFORM_WINDOWS
+ uint64_t Remainder = 0;
+ uint64_t Quotient = _udiv128(uint64_t(1) << S, 0, Divisor, &Remainder);
+ uint64_t M = Quotient + (Remainder ? 1 : 0); // ceil
+#else
+ unsigned __int128 Num = (unsigned __int128)(uint64_t(1) << S) << 64;
+ uint64_t Quotient = uint64_t(Num / Divisor);
+ uint64_t Remainder = uint64_t(Num % Divisor);
+ uint64_t M = Quotient + (Remainder ? 1 : 0);
+#endif
+ if (M != 0)
+ {
+ Mul = M;
+ Shift = S;
+ return;
+ }
+ }
+ // Unreachable for any Divisor > 1.
+ Mul = 0;
+ Shift = 0;
+}
+
//////////////////////////////////////////////////////////////////////////
//
// Testing related code follows...
@@ -68,6 +105,63 @@ TEST_CASE("intmath")
CHECK(ByteSwap(uint64_t(0x214d'6172'7469'6e21ull)) == 0x216e'6974'7261'4d21ull);
}
+TEST_CASE("ReciprocalU64 matches integer division")
+{
+ uint64_t Divisors[] = {1, 2, 3, 4, 5, 7, 10, 100, 1000, 3000, 3579};
+
+ for (uint64_t D : Divisors)
+ {
+ ReciprocalU64 R(D);
+
+ uint64_t TestValues[] = {
+ 0,
+ 1,
+ D - 1,
+ D,
+ D + 1,
+ D * 2,
+ D * 2 + 1,
+ 1'000'000,
+ 10'000'000,
+ 100'000'000,
+ 1'000'000'000ULL,
+ 10'000'000'000ULL,
+ 100'000'000'000ULL,
+ 1'000'000'000'000ULL,
+ uint64_t(~0u),
+ };
+
+ for (uint64_t V : TestValues)
+ {
+ uint32_t Expected = uint32_t(V / D);
+ uint32_t Got = R.Divide(V);
+ CHECK_MESSAGE(Got == Expected, "V=", V, " D=", D, " expected=", Expected, " got=", Got);
+ }
+ }
+}
+
+TEST_CASE("ReciprocalU64 rounding division")
+{
+ // Verify the rounding pattern used in AbsorbBatch: (Cycle + half) / d
+ uint64_t Divisors[] = {3, 4, 5, 10, 3579};
+
+ for (uint64_t D : Divisors)
+ {
+ ReciprocalU64 R(D);
+ uint64_t Half = D >> 1;
+
+ uint64_t TestCycles[] = {0, 1, 100, 999'999, 1'000'000, 99'999'999, 1'000'000'000ULL, 50'000'000'000ULL};
+
+ for (uint64_t Cycle : TestCycles)
+ {
+ uint64_t Rounded = Cycle + Half;
+ uint32_t Expected = uint32_t(Rounded / D);
+ uint32_t Got = R.Divide(Rounded);
+ CHECK_MESSAGE(Got == Expected, "Cycle=", Cycle, " D=", D, " expected=", Expected, " got=", Got);
+ }
+ }
+}
+
TEST_SUITE_END();
#endif
diff --git a/src/zencore/iobuffer.cpp b/src/zencore/iobuffer.cpp
index c47c54981..529afe341 100644
--- a/src/zencore/iobuffer.cpp
+++ b/src/zencore/iobuffer.cpp
@@ -107,7 +107,7 @@ IoBufferCore::~IoBufferCore()
}
void
-IoBufferCore::DeleteThis() const
+IoBufferCore::DeleteThis() const noexcept
{
// We do this just to avoid paying for the cost of a vtable
if (const IoBufferExtendedCore* _ = ExtendedCore())
@@ -210,7 +210,12 @@ IoBufferExtendedCore::~IoBufferExtendedCore()
// Mark file for deletion when final handle is closed
FILE_DISPOSITION_INFO Fdi{.DeleteFile = TRUE};
- SetFileInformationByHandle(m_FileHandle, FileDispositionInfo, &Fdi, sizeof Fdi);
+ if (!SetFileInformationByHandle(m_FileHandle, FileDispositionInfo, &Fdi, sizeof Fdi))
+ {
+ ZEN_WARN("SetFileInformationByHandle(DeleteOnClose) failed for file handle {}, reason '{}'",
+ m_FileHandle,
+ GetLastErrorAsString());
+ }
#else
std::error_code Ec;
std::filesystem::path FilePath = zen::PathFromHandle(m_FileHandle, Ec);
@@ -447,7 +452,7 @@ GetNullBufferCore()
return Core;
}
-RefPtr<IoBufferCore> IoBuffer::NullBufferCore(GetNullBufferCore());
+Ref<IoBufferCore> IoBuffer::NullBufferCore(GetNullBufferCore());
IoBuffer::IoBuffer(size_t InSize) : m_Core(new IoBufferCore(InSize))
{
@@ -475,7 +480,7 @@ IoBuffer::IoBuffer(const IoBuffer& OuterBuffer, size_t Offset, size_t Size)
}
else
{
- m_Core = new IoBufferCore(OuterBuffer.m_Core, reinterpret_cast<const uint8_t*>(OuterBuffer.Data()) + Offset, Size);
+ m_Core = new IoBufferCore(OuterBuffer.m_Core.Get(), reinterpret_cast<const uint8_t*>(OuterBuffer.Data()) + Offset, Size);
}
}
diff --git a/src/zencore/jobqueue.cpp b/src/zencore/jobqueue.cpp
index 3e58fb97d..40e4e2162 100644
--- a/src/zencore/jobqueue.cpp
+++ b/src/zencore/jobqueue.cpp
@@ -12,10 +12,9 @@
#endif // ZEN_WITH_TESTS
ZEN_THIRD_PARTY_INCLUDES_START
+#include <EASTL/deque.h>
#include <gsl/gsl-lite.hpp>
ZEN_THIRD_PARTY_INCLUDES_END
-
-#include <deque>
#include <thread>
#include <unordered_map>
@@ -93,7 +92,7 @@ public:
{
NewJobId = IdGenerator.fetch_add(1);
}
- RefPtr<Job> NewJob(new Job());
+ Ref<Job> NewJob(new Job());
NewJob->Queue = this;
NewJob->Name = Name;
NewJob->Callback = std::move(JobFunc);
@@ -124,7 +123,7 @@ public:
QueueLock.WithExclusiveLock([&]() {
if (auto It = std::find_if(QueuedJobs.begin(),
QueuedJobs.end(),
- [NewJobId](const RefPtr<Job>& Job) { return Job->Id.Id == NewJobId; });
+ [NewJobId](const Ref<Job>& Job) { return Job->Id.Id == NewJobId; });
It != QueuedJobs.end())
{
QueuedJobs.erase(It);
@@ -156,7 +155,7 @@ public:
Result = true;
return;
}
- if (auto It = std::find_if(QueuedJobs.begin(), QueuedJobs.end(), [&Id](const RefPtr<Job>& Job) { return Job->Id.Id == Id.Id; });
+ if (auto It = std::find_if(QueuedJobs.begin(), QueuedJobs.end(), [&Id](const Ref<Job>& Job) { return Job->Id.Id == Id.Id; });
It != QueuedJobs.end())
{
ZEN_DEBUG("Cancelling queued background job {}:'{}'", (*It)->Id.Id, (*It)->Name);
@@ -301,7 +300,7 @@ public:
AbortedJobs.erase(It);
return;
}
- if (auto It = std::find_if(QueuedJobs.begin(), QueuedJobs.end(), [&Id](const RefPtr<Job>& Job) { return Job->Id.Id == Id.Id; });
+ if (auto It = std::find_if(QueuedJobs.begin(), QueuedJobs.end(), [&Id](const Ref<Job>& Job) { return Job->Id.Id == Id.Id; });
It != QueuedJobs.end())
{
Result = Convert(JobStatus::Queued, *(*It));
@@ -340,20 +339,20 @@ public:
std::atomic_uint64_t IdGenerator = 1;
- std::atomic_bool InitializedFlag = false;
- RwLock QueueLock;
- std::deque<RefPtr<Job>> QueuedJobs;
- std::unordered_map<uint64_t, RefPtr<Job>> RunningJobs;
- std::unordered_map<uint64_t, RefPtr<Job>> CompletedJobs;
- std::unordered_map<uint64_t, RefPtr<Job>> AbortedJobs;
+ std::atomic_bool InitializedFlag = false;
+ RwLock QueueLock;
+ eastl::deque<Ref<Job>> QueuedJobs;
+ std::unordered_map<uint64_t, Ref<Job>> RunningJobs;
+ std::unordered_map<uint64_t, Ref<Job>> CompletedJobs;
+ std::unordered_map<uint64_t, Ref<Job>> AbortedJobs;
WorkerThreadPool WorkerPool;
Latch WorkerCounter;
void Worker()
{
- int CurrentThreadId = GetCurrentThreadId();
- RefPtr<Job> CurrentJob;
+ int CurrentThreadId = GetCurrentThreadId();
+ Ref<Job> CurrentJob;
QueueLock.WithExclusiveLock([&]() {
if (!QueuedJobs.empty())
{
diff --git a/src/zencore/logging.cpp b/src/zencore/logging.cpp
index 5ada0cac7..3ec614da1 100644
--- a/src/zencore/logging.cpp
+++ b/src/zencore/logging.cpp
@@ -26,7 +26,7 @@ namespace {
// Bootstrap logger: a minimal stdout logger that exists for the entire lifetime
// of the process. TheDefaultLogger points here before InitializeLogging() runs
// (and is restored here after ShutdownLogging()) so that log macros always have
-// a usable target — no null checks or lazy init required on the common path.
+// a usable target - no null checks or lazy init required on the common path.
zen::Ref<zen::logging::Logger> s_BootstrapLogger = [] {
zen::logging::SinkPtr Sink(new zen::logging::AnsiColorStdoutSink());
return zen::Ref<zen::logging::Logger>(new zen::logging::Logger("", Sink));
@@ -112,6 +112,14 @@ constinit std::string_view LevelNames[] = {std::string_view("trace", 5),
std::string_view("critical", 8),
std::string_view("off", 3)};
+constinit std::string_view ShortNames[] = {std::string_view("trc", 3),
+ std::string_view("dbg", 3),
+ std::string_view("inf", 3),
+ std::string_view("wrn", 3),
+ std::string_view("err", 3),
+ std::string_view("crt", 3),
+ std::string_view("off", 3)};
+
LogLevel
ParseLogLevelString(std::string_view Name)
{
@@ -137,14 +145,29 @@ ParseLogLevelString(std::string_view Name)
}
std::string_view
-ToStringView(LogLevel Level)
+ToString(LogLevel Level)
{
+ using namespace std::literals;
+
if (int(Level) < LogLevelCount)
{
return LevelNames[int(Level)];
}
- return "None";
+ return "None"sv;
+}
+
+std::string_view
+ShortToString(LogLevel Level)
+{
+ using namespace std::literals;
+
+ if (int(Level) < LogLevelCount)
+ {
+ return ShortNames[int(Level)];
+ }
+
+ return "None"sv;
}
} // namespace zen::logging
@@ -476,6 +499,10 @@ LoggerRef::LoggerRef(logging::Logger& InLogger) : m_Logger(static_cast<logging::
{
}
+LoggerRef::LoggerRef(std::string_view LogCategory) : m_Logger(zen::logging::Get(LogCategory).m_Logger)
+{
+}
+
void
LoggerRef::Flush()
{
diff --git a/src/zencore/logging/ansicolorsink.cpp b/src/zencore/logging/ansicolorsink.cpp
index 03aae068a..fb127bede 100644
--- a/src/zencore/logging/ansicolorsink.cpp
+++ b/src/zencore/logging/ansicolorsink.cpp
@@ -5,6 +5,7 @@
#include <zencore/logging/messageonlyformatter.h>
#include <zencore/thread.h>
+#include <zencore/timer.h>
#include <cstdio>
#include <cstdlib>
@@ -22,48 +23,37 @@
namespace zen::logging {
-// Default formatter replicating spdlog's %+ pattern:
-// [YYYY-MM-DD HH:MM:SS.mmm] [logger_name] [level] message\n
+// Default formatter for console output:
+// [HH:MM:SS.mmm] [logger_name] [level] message\n
+// Timestamps show elapsed time since process launch.
class DefaultConsoleFormatter : public Formatter
{
public:
+ DefaultConsoleFormatter() : m_Epoch(std::chrono::system_clock::now() - std::chrono::milliseconds(GetTimeSinceProcessStart())) {}
+
void Format(const LogMessage& Msg, MemoryBuffer& Dest) override
{
- // timestamp
- auto Secs = std::chrono::duration_cast<std::chrono::seconds>(Msg.GetTime().time_since_epoch());
- if (Secs != m_LastLogSecs)
- {
- m_LastLogSecs = Secs;
- m_CachedLocalTm = helpers::SafeLocaltime(LogClock::to_time_t(Msg.GetTime()));
- }
+ // Elapsed time since process launch
+ auto Elapsed = Msg.GetTime() - m_Epoch;
+ auto TotalSecs = std::chrono::duration_cast<std::chrono::seconds>(Elapsed);
+ int Count = static_cast<int>(TotalSecs.count());
+ int LogSecs = Count % 60;
+ Count /= 60;
+ int LogMins = Count % 60;
+ int LogHours = Count / 60;
Dest.push_back('[');
- helpers::AppendInt(m_CachedLocalTm.tm_year + 1900, Dest);
- Dest.push_back('-');
- helpers::Pad2(m_CachedLocalTm.tm_mon + 1, Dest);
- Dest.push_back('-');
- helpers::Pad2(m_CachedLocalTm.tm_mday, Dest);
- Dest.push_back(' ');
- helpers::Pad2(m_CachedLocalTm.tm_hour, Dest);
+ helpers::Pad2(LogHours, Dest);
Dest.push_back(':');
- helpers::Pad2(m_CachedLocalTm.tm_min, Dest);
+ helpers::Pad2(LogMins, Dest);
Dest.push_back(':');
- helpers::Pad2(m_CachedLocalTm.tm_sec, Dest);
+ helpers::Pad2(LogSecs, Dest);
Dest.push_back('.');
auto Millis = helpers::TimeFraction<std::chrono::milliseconds>(Msg.GetTime());
helpers::Pad3(static_cast<uint32_t>(Millis.count()), Dest);
Dest.push_back(']');
Dest.push_back(' ');
- // logger name
- if (Msg.GetLoggerName().size() > 0)
- {
- Dest.push_back('[');
- helpers::AppendStringView(Msg.GetLoggerName(), Dest);
- Dest.push_back(']');
- Dest.push_back(' ');
- }
-
// level
Dest.push_back('[');
if (IsColorEnabled())
@@ -78,6 +68,25 @@ public:
Dest.push_back(']');
Dest.push_back(' ');
+ using namespace std::literals;
+
+ // logger name
+ if (Msg.GetLoggerName().size() > 0)
+ {
+ if (IsColorEnabled())
+ {
+ Dest.append("\033[97m"sv);
+ }
+ Dest.push_back('[');
+ helpers::AppendStringView(Msg.GetLoggerName(), Dest);
+ Dest.push_back(']');
+ if (IsColorEnabled())
+ {
+ Dest.append("\033[0m"sv);
+ }
+ Dest.push_back(' ');
+ }
+
// message (align continuation lines with the first line)
size_t AnsiBytes = IsColorEnabled() ? (helpers::AnsiColorForLevel(Msg.GetLevel()).size() + helpers::kAnsiReset.size()) : 0;
size_t LinePrefixCount = Dest.size() - AnsiBytes;
@@ -128,8 +137,7 @@ public:
}
private:
- std::chrono::seconds m_LastLogSecs{0};
- std::tm m_CachedLocalTm{};
+ LogClock::time_point m_Epoch;
};
bool
@@ -197,7 +205,7 @@ IsColorTerminal()
// Windows console supports ANSI color by default in modern versions
return true;
#else
- // Unknown terminal — be conservative
+ // Unknown terminal - be conservative
return false;
#endif
}
diff --git a/src/zencore/logging/logger.cpp b/src/zencore/logging/logger.cpp
index ff1db3edc..830d2b611 100644
--- a/src/zencore/logging/logger.cpp
+++ b/src/zencore/logging/logger.cpp
@@ -1,6 +1,7 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include <zencore/logging/logger.h>
+#include <zencore/logging/tracelog.h>
#include <zencore/thread.h>
#include <string>
@@ -33,6 +34,12 @@ Logger::Log(const LogPoint& Point, fmt::format_args Args)
fmt::basic_memory_buffer<char, 250> Buffer;
fmt::vformat_to(fmt::appender(Buffer), Point.FormatString, Args);
+#if ZEN_WITH_TRACE
+ // Forward the typed argument pack to the trace system while we still have
+ // it - sinks are invoked below with the already-rendered payload.
+ TraceLogTyped(*this, Point, Args);
+#endif
+
LogMessage LogMsg(Point, m_Impl->m_Name, std::string_view(Buffer.data(), Buffer.size()));
LogMsg.SetThreadId(GetCurrentThreadId());
SinkIt(LogMsg);
diff --git a/src/zencore/logging/registry.cpp b/src/zencore/logging/registry.cpp
index 383a5d8ba..0f552aced 100644
--- a/src/zencore/logging/registry.cpp
+++ b/src/zencore/logging/registry.cpp
@@ -137,7 +137,7 @@ struct Registry::Impl
{
if (Pattern.find_first_of("*?") == std::string::npos)
{
- // Exact match — fast path via map lookup.
+ // Exact match - fast path via map lookup.
auto It = m_Loggers.find(Pattern);
if (It != m_Loggers.end())
{
@@ -146,7 +146,7 @@ struct Registry::Impl
}
else
{
- // Wildcard pattern — iterate all loggers.
+ // Wildcard pattern - iterate all loggers.
for (auto& [Name, CurLogger] : m_Loggers)
{
if (MatchLoggerPattern(Pattern, Name))
diff --git a/src/zencore/logging/tracelog.cpp b/src/zencore/logging/tracelog.cpp
new file mode 100644
index 000000000..d90b07f02
--- /dev/null
+++ b/src/zencore/logging/tracelog.cpp
@@ -0,0 +1,800 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zencore/logbase.h>
+#include <zencore/logging/logger.h>
+#include <zencore/logging/tracelog.h>
+#include <zencore/testing.h>
+#include <zencore/thread.h>
+#include <zencore/timer.h>
+#include <zencore/trace.h>
+
+#if ZEN_WITH_TRACE
+
+ZEN_THIRD_PARTY_INCLUDES_START
+# include <EASTL/fixed_vector.h>
+# include <fmt/args.h>
+# include <fmt/format.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+# include <cstring>
+# include <limits>
+# include <unordered_set>
+
+namespace zen::logging {
+
+UE_TRACE_CHANNEL_DEFINE(ZenLogChannel)
+
+// A zen-specific log event schema. The field layout is deliberately close to
+// UE's Logging.* events so the analyzer's plumbing looks familiar, but the
+// event names are distinct because our FormatArgs blob is formatted against
+// an fmt-style `{}` template instead of a printf-style `%d`/`%s`/... template,
+// and Insights' current log analyzer can't render the former.
+UE_TRACE_EVENT_BEGIN(ZenLog, Category, NoSync | Important)
+ UE_TRACE_EVENT_FIELD(const void*, CategoryPointer)
+ UE_TRACE_EVENT_FIELD(uint8_t, DefaultVerbosity)
+ UE_TRACE_EVENT_FIELD(UE::Trace::AnsiString, Name)
+UE_TRACE_EVENT_END()
+
+UE_TRACE_EVENT_BEGIN(ZenLog, MessageSpec, NoSync | Important)
+ UE_TRACE_EVENT_FIELD(const void*, LogPoint)
+ UE_TRACE_EVENT_FIELD(const void*, CategoryPointer)
+ UE_TRACE_EVENT_FIELD(int32_t, Line)
+ UE_TRACE_EVENT_FIELD(uint8_t, Verbosity)
+ UE_TRACE_EVENT_FIELD(UE::Trace::AnsiString, FileName)
+ UE_TRACE_EVENT_FIELD(UE::Trace::AnsiString, FormatString)
+UE_TRACE_EVENT_END()
+
+UE_TRACE_EVENT_BEGIN(ZenLog, Message, NoSync)
+ UE_TRACE_EVENT_FIELD(const void*, LogPoint)
+ UE_TRACE_EVENT_FIELD(uint64_t, Cycle)
+ UE_TRACE_EVENT_FIELD(uint8_t[], FormatArgs)
+UE_TRACE_EVENT_END()
+
+namespace {
+
+ // Translate zen's LogLevel to UE's ELogVerbosity convention, which is what
+ // trace consumers (Unreal Insights, the zen viewer, ...) expect on the wire.
+ // zen: Trace=0, Debug=1, Info=2, Warn=3, Err=4, Critical=5, Off=6
+ // UE: NoLogging=0, Fatal=1, Error=2, Warning=3, Display=4, Log=5, Verbose=6, VeryVerbose=7
+ uint8_t ToUeVerbosity(LogLevel Level)
+ {
+ switch (Level)
+ {
+ case Trace:
+ return 7; // VeryVerbose
+ case Debug:
+ return 6; // Verbose
+ case Info:
+ return 5; // Log
+ case Warn:
+ return 3; // Warning
+ case Err:
+ return 2; // Error
+ case Critical:
+ return 1; // Fatal
+ case Off:
+ default:
+ return 0; // NoLogging
+ }
+ }
+
+ // Descriptor byte layout: top 3 bits are a category tag, bottom 5 bits are
+ // the payload size in bytes (for strings, the per-character width). Sizes
+ // are always <= 8 in practice, so 5 bits is plenty; the extra category
+ // bit leaves room for future types (timestamps, blobs, compact integers,
+ // ...) without another wire-format break.
+ //
+ // Note: this is the ZenLog-specific descriptor encoding. UE's upstream
+ // Logging.* events (decoded by FormatLogMessage in trace_model.cpp) use a
+ // 2-bit category / 6-bit size layout, which is a separate wire format.
+ //
+ // Bool category (0x00): size = 1, payload byte is 0 or 1
+ // Signed int category (0x20): size = 1/2/4/8, payload is two's complement
+ // Unsigned int category(0x40): size = 1/2/4/8, payload is unsigned
+ // Float category (0x60): float/double, size = 4/8
+ // String category (0x80): size = 1 (ANSI char), payload is null-terminated
+ // Pointer category (0xA0): size = 8, payload is the raw address
+ // (0xC0, 0xE0 are reserved for future types)
+ constexpr uint8_t kCatMask = 0xE0;
+ constexpr uint8_t kSizeMask = 0x1F;
+ constexpr uint8_t kCatBool = 0x00;
+ constexpr uint8_t kCatSInt = 0x20;
+ constexpr uint8_t kCatUInt = 0x40;
+ constexpr uint8_t kCatFloat = 0x60;
+ constexpr uint8_t kCatString = 0x80;
+ constexpr uint8_t kCatPointer = 0xA0;
+
+ // A single byte in the wire header holds the argument count, so 255 is the
+ // hard cap. Any log site emitting more args is truncated silently; no real
+ // log line should come close.
+ constexpr size_t kMaxEncodedArgCount = 0xff;
+
+ struct ArgEncoder
+ {
+ // Descriptors and payload are built side by side and spliced together by
+ // the caller once all args have been visited: the wire format wants all
+ // descriptors before any payload bytes, but we only learn each arg's
+ // size as we visit it.
+ eastl::fixed_vector<uint8_t, 64> Descriptors;
+ eastl::fixed_vector<uint8_t, 512> Payload;
+
+ void PushBool(bool Value)
+ {
+ Descriptors.push_back(uint8_t(kCatBool | 1));
+ Payload.push_back(Value ? uint8_t(1) : uint8_t(0));
+ }
+
+ void PushSInt(uint64_t RawBits, uint8_t ByteSize)
+ {
+ Descriptors.push_back(uint8_t(kCatSInt | ByteSize));
+ AppendRaw(&RawBits, ByteSize);
+ }
+
+ void PushUInt(uint64_t RawBits, uint8_t ByteSize)
+ {
+ Descriptors.push_back(uint8_t(kCatUInt | ByteSize));
+ AppendRaw(&RawBits, ByteSize);
+ }
+
+ void PushFloat(const void* Src, uint8_t ByteSize)
+ {
+ Descriptors.push_back(uint8_t(kCatFloat | ByteSize));
+ AppendRaw(Src, ByteSize);
+ }
+
+ void PushString(std::string_view Str)
+ {
+ Descriptors.push_back(uint8_t(kCatString | 1));
+ const uint8_t* Begin = reinterpret_cast<const uint8_t*>(Str.data());
+ Payload.insert(Payload.end(), Begin, Begin + Str.size());
+ Payload.push_back(uint8_t(0));
+ }
+
+ void PushPointer(const void* Ptr)
+ {
+ Descriptors.push_back(uint8_t(kCatPointer | 8));
+ uint64_t Raw = uint64_t(reinterpret_cast<uintptr_t>(Ptr));
+ AppendRaw(&Raw, sizeof(Raw));
+ }
+
+ void AppendRaw(const void* Src, size_t Size)
+ {
+ const uint8_t* Bytes = static_cast<const uint8_t*>(Src);
+ Payload.insert(Payload.end(), Bytes, Bytes + Size);
+ }
+
+ // fmt::basic_format_arg::visit dispatches on these overloads.
+ void operator()(fmt::monostate) {}
+ void operator()(bool v) { PushBool(v); }
+ void operator()(char v) { PushSInt(uint64_t(int8_t(v)), 1); }
+ void operator()(int v) { PushSInt(uint64_t(int64_t(v)), 4); }
+ void operator()(unsigned v) { PushUInt(uint64_t(v), 4); }
+ void operator()(long long v) { PushSInt(uint64_t(v), 8); }
+ void operator()(unsigned long long v) { PushUInt(v, 8); }
+# if FMT_USE_INT128
+ // Trace wire format tops out at 64-bit ints; truncate. Without these,
+ // overload resolution on __int128_t is ambiguous between long long and
+ // unsigned long long under clang's fmt::visit dispatch.
+ void operator()(fmt::detail::int128_opt v) { PushSInt(uint64_t(int64_t(v)), 8); }
+ void operator()(fmt::detail::uint128_opt v) { PushUInt(uint64_t(v), 8); }
+# endif
+ void operator()(float v) { PushFloat(&v, 4); }
+ void operator()(double v) { PushFloat(&v, 8); }
+ void operator()(long double v)
+ {
+ // Down-cast to double: the trace decoder only knows 4/8-byte floats,
+ // and long double is rare in log sites.
+ double d = double(v);
+ PushFloat(&d, 8);
+ }
+ void operator()(const char* v) { PushString(v ? std::string_view(v) : std::string_view{}); }
+ void operator()(fmt::string_view v) { PushString({v.data(), v.size()}); }
+ void operator()(const void* v) { PushPointer(v); }
+ void operator()(fmt::basic_format_arg<fmt::context>::handle CustomHandle)
+ {
+ // fmt has no public way to inspect a custom-formatted value's type,
+ // so for user-defined formatters we render via fmt itself and ship
+ // the result as a string argument. This preserves message fidelity
+ // at the cost of losing typed-arg introspection for custom types.
+ //
+ // vformat_to will already have rendered this arg successfully for the
+ // regular sinks above us, so the formatter is expected to succeed here
+ // too. The try/catch is defensive: a custom formatter that relies on
+ // something unusual in its context could still misbehave, and a stray
+ // exception here would unwind through every ZEN_LOG caller.
+ fmt::memory_buffer Scratch;
+ fmt::parse_context<char> ParseCtx{fmt::string_view("")};
+ fmt::context FmtCtx{fmt::appender(Scratch), fmt::format_args{}};
+ try
+ {
+ CustomHandle.format(ParseCtx, FmtCtx);
+ PushString({Scratch.data(), Scratch.size()});
+ }
+ catch (...)
+ {
+ PushString("<handle error>");
+ }
+ }
+ };
+
+ // Dedupe state for the Important events. Both LogCategory and MessageSpec are
+ // cached by the trace runtime and replayed to consumers that connect later,
+ // so we only want to emit them once per distinct (logger, point) pair.
+ struct EmitState
+ {
+ RwLock Lock;
+ std::unordered_set<const void*> EmittedCategories;
+ std::unordered_set<const void*> EmittedSpecs;
+ };
+
+ EmitState& GetEmitState()
+ {
+ static EmitState State;
+ return State;
+ }
+
+ void EmitCategory(const void* CategoryPtr, std::string_view Name, LogLevel DefaultVerbosity)
+ {
+ const uint16_t NameLen = uint16_t(Name.size());
+ UE_TRACE_LOG(ZenLog, Category, ZenLogChannel, NameLen * sizeof(ANSICHAR))
+ << Category.CategoryPointer(CategoryPtr) << Category.DefaultVerbosity(ToUeVerbosity(DefaultVerbosity))
+ << Category.Name(Name.data(), NameLen);
+ }
+
+ void EmitMessageSpec(const void* Point,
+ const void* CategoryPtr,
+ LogLevel Verbosity,
+ std::string_view File,
+ int32_t Line,
+ std::string_view Format)
+ {
+ const uint16_t FileNameLen = uint16_t(File.size());
+ const uint16_t FormatStringLen = uint16_t(Format.size());
+ const uint32_t DataSize = (FileNameLen + FormatStringLen) * sizeof(ANSICHAR);
+ UE_TRACE_LOG(ZenLog, MessageSpec, ZenLogChannel, DataSize)
+ << MessageSpec.LogPoint(Point) << MessageSpec.CategoryPointer(CategoryPtr) << MessageSpec.Line(Line)
+ << MessageSpec.Verbosity(ToUeVerbosity(Verbosity)) << MessageSpec.FileName(File.data(), FileNameLen)
+ << MessageSpec.FormatString(Format.data(), FormatStringLen);
+ }
+
+ void EmitMessage(const void* Point, const uint8_t* EncodedArgs, int32_t EncodedSize)
+ {
+ UE_TRACE_LOG(ZenLog, Message, ZenLogChannel)
+ << Message.LogPoint(Point) << Message.Cycle(GetHifreqTimerValue()) << Message.FormatArgs(EncodedArgs, EncodedSize);
+ }
+
+ // Visit every arg in the pack (up to the 255-arg wire cap) and feed them
+ // into the encoder. Factored out so both the hot path (TraceLogTyped, with
+ // a stack buffer) and the public EncodeLogArgs (with a std::vector) share
+ // a single arg-walking implementation.
+ void VisitAllArgs(fmt::format_args Args, ArgEncoder& Enc)
+ {
+ const int ArgLimit = std::min<int>(Args.max_size(), int(kMaxEncodedArgCount));
+ for (int i = 0; i < ArgLimit; ++i)
+ {
+ fmt::basic_format_arg<fmt::context> Arg = Args.get(i);
+ if (!Arg)
+ {
+ break;
+ }
+ Arg.visit(Enc);
+ }
+ }
+
+} // namespace
+
+void
+TraceLogTyped(const Logger& InLogger, const LogPoint& Point, fmt::format_args Args)
+{
+ if (!UE_TRACE_CHANNELEXPR_IS_ENABLED(ZenLogChannel))
+ {
+ return;
+ }
+
+ // The logger's name is stored in a std::string that's written once in the
+ // constructor and never reassigned, so its .data() pointer is stable for
+ // the logger's lifetime and is unique per Logger instance - exactly what
+ // we need to identify a category on the wire.
+ const std::string_view LoggerName = InLogger.Name();
+ const void* CategoryPtr = LoggerName.data();
+ if (CategoryPtr == nullptr)
+ {
+ return;
+ }
+
+ EmitState& State = GetEmitState();
+
+ bool NeedsEmit = false;
+ {
+ RwLock::SharedLockScope Read(State.Lock);
+ NeedsEmit = State.EmittedCategories.find(CategoryPtr) == State.EmittedCategories.end() ||
+ State.EmittedSpecs.find(&Point) == State.EmittedSpecs.end();
+ }
+ if (NeedsEmit)
+ {
+ RwLock::ExclusiveLockScope Write(State.Lock);
+ if (State.EmittedCategories.insert(CategoryPtr).second)
+ {
+ EmitCategory(CategoryPtr, LoggerName, Point.Level);
+ }
+ if (State.EmittedSpecs.insert(&Point).second)
+ {
+ const std::string_view File = Point.Filename ? std::string_view(Point.Filename) : std::string_view{};
+ EmitMessageSpec(&Point, CategoryPtr, Point.Level, File, Point.Line, Point.FormatString);
+ }
+ }
+
+ ArgEncoder Enc;
+ VisitAllArgs(Args, Enc);
+
+ // Splice the wire layout: [ArgumentCount:uint8][Descriptors...][Payload...].
+ // Sized to cover the common case without touching the heap; oversize args
+ // spill to the fixed_vector's fallback allocator.
+ eastl::fixed_vector<uint8_t, 1 + 64 + 512> Blob;
+ Blob.reserve(1 + Enc.Descriptors.size() + Enc.Payload.size());
+ Blob.push_back(uint8_t(Enc.Descriptors.size()));
+ Blob.insert(Blob.end(), Enc.Descriptors.begin(), Enc.Descriptors.end());
+ Blob.insert(Blob.end(), Enc.Payload.begin(), Enc.Payload.end());
+
+ EmitMessage(&Point, Blob.data(), int32_t(Blob.size()));
+}
+
+void
+EncodeLogArgs(fmt::format_args Args, std::vector<uint8_t>& Out)
+{
+ ArgEncoder Enc;
+ VisitAllArgs(Args, Enc);
+
+ // Wire layout: [ArgumentCount:uint8][Descriptors:count][Payload:variable].
+ // VisitAllArgs breaks at the first arg of type none_type (what `!Arg`
+ // tests), which is how fmt signals "past the end" of the pack. Every other
+ // overload pushes exactly one descriptor, so the descriptor count is the
+ // authoritative argument count for the wire.
+ Out.clear();
+ Out.reserve(1 + Enc.Descriptors.size() + Enc.Payload.size());
+ Out.push_back(uint8_t(Enc.Descriptors.size()));
+ Out.insert(Out.end(), Enc.Descriptors.begin(), Enc.Descriptors.end());
+ Out.insert(Out.end(), Enc.Payload.begin(), Enc.Payload.end());
+}
+
+namespace {
+
+ // Cursor over a ZenLog FormatArgs blob. The layout is
+ // `[count:uint8][descriptors:count][payload]` - see EncodeLogArgs.
+ struct ArgDecoder
+ {
+ const uint8_t* Descriptors = nullptr;
+ const uint8_t* Payload = nullptr;
+ uint8_t Remaining = 0;
+
+ bool HasNext() const { return Remaining > 0; }
+ uint8_t PeekCategory() const { return (*Descriptors) & kCatMask; }
+ uint8_t PeekSize() const { return (*Descriptors) & kSizeMask; }
+
+ void Advance(size_t PayloadBytes)
+ {
+ Payload += PayloadBytes;
+ ++Descriptors;
+ --Remaining;
+ }
+
+ // On success, wires up the cursor into Data. On a null/truncated blob,
+ // leaves Dec in its default state (Remaining == 0) so the decode loop
+ // becomes a no-op and the caller just renders the format template
+ // against an empty arg store.
+ static void Init(ArgDecoder& Dec, const uint8_t* Data, size_t Size)
+ {
+ if (!Data || Size == 0)
+ {
+ return;
+ }
+ const uint8_t Count = Data[0];
+ if (size_t(1) + Count > Size)
+ {
+ return;
+ }
+ Dec.Descriptors = Data + 1;
+ Dec.Payload = Data + 1 + Count;
+ Dec.Remaining = Count;
+ }
+ };
+
+} // namespace
+
+std::string
+FormatLogArgs(std::string_view Format, const uint8_t* Data, size_t Size)
+{
+ using namespace std::literals;
+
+ ArgDecoder Stream;
+ ArgDecoder::Init(Stream, Data, Size);
+
+ // Payload bytes (including inlined null-terminated strings) live in the
+ // caller-owned Data buffer for the duration of this call, so we can hand
+ // fmt std::string_views that point straight into it rather than copying.
+ fmt::dynamic_format_arg_store<fmt::format_context> Store;
+ Store.reserve(Stream.Remaining, /*named*/ 0);
+
+ while (Stream.HasNext())
+ {
+ const uint8_t Category = Stream.PeekCategory();
+ const uint8_t ArgSize = Stream.PeekSize();
+
+ if (Category == kCatBool)
+ {
+ bool Value = (ArgSize == 1) && (Stream.Payload[0] != 0);
+ // Pushing as bool rather than int means fmt renders `{}` as
+ // `true`/`false` and `{:d}` as `1`/`0`, matching what the
+ // console/file sinks showed.
+ Store.push_back(Value);
+ Stream.Advance(ArgSize);
+ }
+ else if (Category == kCatSInt)
+ {
+ uint64_t Raw = 0;
+ if (ArgSize > 0 && ArgSize <= sizeof(Raw))
+ {
+ std::memcpy(&Raw, Stream.Payload, ArgSize);
+ }
+ // Sign-extend from the stored width so narrower negative values
+ // round-trip correctly.
+ int64_t Signed = 0;
+ switch (ArgSize)
+ {
+ case 1:
+ Signed = int8_t(Raw & 0xff);
+ break;
+ case 2:
+ Signed = int16_t(Raw & 0xffff);
+ break;
+ case 4:
+ Signed = int32_t(Raw & 0xffffffff);
+ break;
+ case 8:
+ default:
+ Signed = int64_t(Raw);
+ break;
+ }
+ Store.push_back(Signed);
+ Stream.Advance(ArgSize);
+ }
+ else if (Category == kCatUInt)
+ {
+ uint64_t Raw = 0;
+ if (ArgSize > 0 && ArgSize <= sizeof(Raw))
+ {
+ std::memcpy(&Raw, Stream.Payload, ArgSize);
+ }
+ Store.push_back(Raw);
+ Stream.Advance(ArgSize);
+ }
+ else if (Category == kCatFloat)
+ {
+ if (ArgSize == 4)
+ {
+ // Preserve float-vs-double distinction so fmt's default `{}`
+ // rendering matches what a direct fmt::format call would show
+ // (double's default precision differs from float's).
+ float F = 0.0f;
+ std::memcpy(&F, Stream.Payload, 4);
+ Store.push_back(F);
+ }
+ else if (ArgSize == 8)
+ {
+ double D = 0.0;
+ std::memcpy(&D, Stream.Payload, 8);
+ Store.push_back(D);
+ }
+ Stream.Advance(ArgSize);
+ }
+ else if (Category == kCatString)
+ {
+ if (ArgSize == 1)
+ {
+ const char* S = reinterpret_cast<const char*>(Stream.Payload);
+ const size_t Len = std::strlen(S);
+ Store.push_back(std::string_view(S, Len));
+ Stream.Advance(Len + 1);
+ }
+ else
+ {
+ // Wider strings (e.g. UTF-16) aren't emitted by the current
+ // encoder and we have no way to tell how many payload bytes
+ // to skip, so further decoding would read garbage. Surface a
+ // placeholder and stop - partial output is better than mis-
+ // aligned output.
+ Store.push_back("<?>"sv);
+ Stream.Remaining = 0;
+ }
+ }
+ else if (Category == kCatPointer)
+ {
+ uint64_t Raw = 0;
+ if (ArgSize > 0 && ArgSize <= sizeof(Raw))
+ {
+ std::memcpy(&Raw, Stream.Payload, ArgSize);
+ }
+ // Round-trip as void* so fmt's default `{}` renders "0x..." hex,
+ // matching a direct fmt::format call against the original pointer.
+ Store.push_back(reinterpret_cast<const void*>(static_cast<uintptr_t>(Raw)));
+ Stream.Advance(ArgSize);
+ }
+ else
+ {
+ // Unknown category: ArgSize is untrustworthy (it may not correspond
+ // to real payload bytes), so trying to continue past this point
+ // would desync the cursor. Stop with a placeholder.
+ Store.push_back("<arg>"sv);
+ Stream.Remaining = 0;
+ }
+ }
+
+ try
+ {
+ return fmt::vformat(fmt::string_view(Format.data(), Format.size()), Store);
+ }
+ catch (const fmt::format_error& Ex)
+ {
+ // A mismatched format spec shouldn't wedge the whole trace view -
+ // surface the template and the error to the reader instead.
+ return fmt::format("<fmt error: {}> {}", Ex.what(), Format);
+ }
+}
+
+} // namespace zen::logging
+
+# if ZEN_WITH_TESTS
+
+namespace zen::logging::tests {
+
+// Round-trip an argument pack through EncodeLogArgs -> FormatLogArgs and
+// compare against a direct fmt::format call against the same template.
+template<typename... Args>
+static void
+CheckRoundtrip(std::string_view Spec, const Args&... InArgs)
+{
+ const std::string Direct = fmt::format(fmt::runtime(Spec), InArgs...);
+
+ std::vector<uint8_t> Blob;
+ EncodeLogArgs(fmt::make_format_args(InArgs...), Blob);
+ const std::string Decoded = FormatLogArgs(Spec, Blob.data(), Blob.size());
+
+ CHECK_MESSAGE(Decoded == Direct, "spec=\"", Spec, "\" direct=\"", Direct, "\" decoded=\"", Decoded, "\"");
+}
+
+TEST_SUITE_BEGIN("core.tracelog");
+
+TEST_CASE("encode.plain_args")
+{
+ CheckRoundtrip("{}", 42);
+ CheckRoundtrip("{}", -42);
+ CheckRoundtrip("{}", 0);
+ CheckRoundtrip("{}", int64_t(0x7fffffffffffffffLL));
+ CheckRoundtrip("{}", std::numeric_limits<int64_t>::min());
+ CheckRoundtrip("{}", uint32_t(4000000000u)); // top bit set - splits signed/unsigned paths
+ CheckRoundtrip("{}", uint64_t(0xffffffffffffffffull));
+ CheckRoundtrip("{}", true);
+ CheckRoundtrip("{}", false);
+ CheckRoundtrip("{}", std::string_view("hello"));
+ CheckRoundtrip("{}", std::string_view(""));
+}
+
+TEST_CASE("encode.width_and_precision")
+{
+ CheckRoundtrip("{:10}", 42);
+ CheckRoundtrip("{:>10}", 42);
+ CheckRoundtrip("{:<10}", 42);
+ CheckRoundtrip("{:^10}", 42);
+ CheckRoundtrip("{:_>10}", 42);
+ CheckRoundtrip("{:*<8}", std::string_view("hi"));
+ CheckRoundtrip("{:.3}", std::string_view("hello"));
+ CheckRoundtrip("{:10.3}", std::string_view("hello"));
+ CheckRoundtrip("{:>10.3}", std::string_view("hello"));
+}
+
+TEST_CASE("encode.type_specs")
+{
+ CheckRoundtrip("{:x}", 255);
+ CheckRoundtrip("{:X}", 255);
+ CheckRoundtrip("{:#x}", 255);
+ CheckRoundtrip("{:b}", 13);
+ CheckRoundtrip("{:#b}", 13);
+ CheckRoundtrip("{:o}", 8);
+ CheckRoundtrip("{:d}", 42);
+ CheckRoundtrip("{:08x}", 0xabc);
+ CheckRoundtrip("{:+d}", 42);
+ CheckRoundtrip("{:+d}", -42);
+ CheckRoundtrip("{: d}", 42);
+}
+
+TEST_CASE("encode.bool_type_specs")
+{
+ // Bool roundtrips as bool (not int), so `{}` gives true/false and `{:d}`
+ // gives 1/0. This is zen-specific vs upstream UE's Logging.* printf path.
+ CheckRoundtrip("{}", true);
+ CheckRoundtrip("{}", false);
+ CheckRoundtrip("{:d}", true);
+ CheckRoundtrip("{:d}", false);
+ CheckRoundtrip("{:s}", true);
+}
+
+TEST_CASE("encode.float_specs")
+{
+ CheckRoundtrip("{}", 3.14);
+ CheckRoundtrip("{}", -3.14);
+ CheckRoundtrip("{}", 0.0);
+ CheckRoundtrip("{:.3f}", 3.14159);
+ CheckRoundtrip("{:10.3f}", 3.14159);
+ CheckRoundtrip("{:>10.3f}", 3.14159);
+ CheckRoundtrip("{:e}", 1234.5678);
+ CheckRoundtrip("{:.2e}", 1234.5678);
+ CheckRoundtrip("{:g}", 1234.5678);
+ CheckRoundtrip("{:+.3f}", 3.14);
+
+ // Float vs double are distinct on the wire so default `{}` renders match
+ CheckRoundtrip("{}", 3.14f);
+ CheckRoundtrip("{:.3f}", 3.14f);
+}
+
+TEST_CASE("encode.nested_widths")
+{
+ // Width/precision passed as separate positional args
+ CheckRoundtrip("{:>{}}", std::string_view("hi"), 8);
+ CheckRoundtrip("{:<{}}", 42, 6);
+ CheckRoundtrip("{:{}}", 42, 8);
+ CheckRoundtrip("{:.{}}", std::string_view("hello"), 3);
+ CheckRoundtrip("{:{}.{}f}", 3.14159, 10, 2);
+}
+
+TEST_CASE("encode.dynamic_width_from_stream")
+{
+ SUBCASE("width only")
+ {
+ // fmt consumes a second arg from the stream for `{}` width. The decoder
+ // must preserve arg ordering and integer type for dynamic specs to work.
+ CheckRoundtrip("{:{}}", 42, 0);
+ CheckRoundtrip("{:{}}", 42, 1); // width narrower than value -> no padding
+ CheckRoundtrip("{:{}}", 42, 12); // wide padding
+ CheckRoundtrip("{:>{}}", std::string_view("x"), 0);
+ CheckRoundtrip("{:>{}}", std::string_view("x"), 16);
+ CheckRoundtrip("{:_^{}}", std::string_view("hi"), 10);
+ }
+ SUBCASE("precision only")
+ {
+ CheckRoundtrip("{:.{}}", std::string_view("abcdef"), 0);
+ CheckRoundtrip("{:.{}}", std::string_view("abcdef"), 3);
+ CheckRoundtrip("{:.{}}", std::string_view("abcdef"), 100);
+ CheckRoundtrip("{:.{}f}", 1.0 / 3.0, 0);
+ CheckRoundtrip("{:.{}f}", 1.0 / 3.0, 6);
+ CheckRoundtrip("{:.{}f}", 1.0 / 3.0, 15);
+ CheckRoundtrip("{:.{}e}", 1234567.89, 3);
+ }
+ SUBCASE("width and precision both dynamic")
+ {
+ CheckRoundtrip("{:{}.{}f}", 3.14159, 10, 2);
+ CheckRoundtrip("{:>{}.{}f}", 3.14159, 14, 4);
+ CheckRoundtrip("{:*>{}.{}}", std::string_view("hello world"), 20, 7);
+ }
+ SUBCASE("positional dynamic width")
+ {
+ // `{0}` / `{1}` / `{2}` refer to specific arg indices; widths sourced
+ // from one index while the value comes from another exercises arg
+ // reordering through the decoder's Store.
+ CheckRoundtrip("{1:>{0}}", 10, std::string_view("xyz"));
+ CheckRoundtrip("{2:{0}.{1}f}", 12, 3, 1.61803);
+ }
+ SUBCASE("multiple values sharing a dynamic width")
+ {
+ // Same width arg consumed by two format specs - every spec needs its
+ // own `{n}` reference to the width.
+ CheckRoundtrip("[{0:>{2}}][{1:>{2}}]", 1, 2, 6);
+ }
+ SUBCASE("dynamic width on unsigned")
+ {
+ // Covers the kCatUInt decoding path in combination with nested specs.
+ CheckRoundtrip("{:{}}", uint32_t(4000000000u), 12);
+ CheckRoundtrip("{:>{}x}", uint64_t(0xdeadbeefcafeull), 16);
+ }
+ SUBCASE("dynamic width of zero")
+ {
+ // fmt treats width=0 as "no minimum width", not as truncation -
+ // ensure the decoder doesn't accidentally special-case it.
+ CheckRoundtrip("[{:>{}}]", std::string_view("hello"), 0);
+ CheckRoundtrip("[{:{}}]", 42, 0);
+ }
+}
+
+TEST_CASE("encode.multiple_args")
+{
+ CheckRoundtrip("{} + {} = {}", 1, 2, 3);
+ CheckRoundtrip("{} {} {} {}", 42, 3.14, std::string_view("str"), true);
+ CheckRoundtrip("[{:>4}] [{:<4}] [{:^4}]", 1, 2, 3);
+}
+
+TEST_CASE("encode.positional_args")
+{
+ CheckRoundtrip("{0} {1} {0}", std::string_view("a"), std::string_view("b"));
+ CheckRoundtrip("{1:<{0}}", 8, std::string_view("hi"));
+}
+
+TEST_CASE("encode.empty_args")
+{
+ std::vector<uint8_t> Blob;
+ EncodeLogArgs(fmt::format_args{}, Blob);
+ REQUIRE(!Blob.empty());
+ CHECK_EQ(Blob[0], uint8_t(0)); // arg count = 0
+ CHECK_EQ(FormatLogArgs("no args here", Blob.data(), Blob.size()), "no args here");
+ CHECK_EQ(FormatLogArgs("literal {{braces}}", Blob.data(), Blob.size()), "literal {braces}");
+}
+
+TEST_CASE("encode.mismatched_format_is_soft_error")
+{
+ // Arg count mismatches shouldn't crash - the decoder surfaces the error in
+ // the rendered message rather than throwing.
+ int Arg = 42;
+
+ std::vector<uint8_t> Blob;
+ EncodeLogArgs(fmt::make_format_args(Arg), Blob);
+ const std::string Out = FormatLogArgs("{} {} {}", Blob.data(), Blob.size());
+ CHECK_MESSAGE(Out.find("fmt error") != std::string::npos, "expected soft error, got: ", Out);
+}
+
+TEST_CASE("encode.pointer")
+{
+ // Pointers are encoded as uint64 and render identically via `{}`.
+ int Local = 0;
+ const void* P = &Local;
+ CheckRoundtrip("{}", P);
+ CheckRoundtrip("{}", static_cast<const void*>(nullptr));
+}
+
+// Minimal user-defined type with a custom fmt formatter, to exercise the
+// handle path in ArgEncoder without pulling in fmt/chrono.h here.
+struct Point2D
+{
+ int X;
+ int Y;
+};
+
+} // namespace zen::logging::tests
+
+template<>
+struct fmt::formatter<zen::logging::tests::Point2D> : fmt::formatter<std::string_view>
+{
+ auto format(const zen::logging::tests::Point2D& P, fmt::format_context& Ctx) const
+ {
+ return fmt::format_to(Ctx.out(), "({},{})", P.X, P.Y);
+ }
+};
+
+namespace zen::logging::tests {
+
+TEST_CASE("encode.custom_formatter_renders_as_string")
+{
+ // Custom-formatted types take the handle path in ArgEncoder, which
+ // pre-renders them to a string using an *empty* parse context. The
+ // decoder then receives a string, so the original `{}` works but any
+ // spec that only makes sense for the original type would fail.
+ Point2D P{3, 4};
+
+ std::vector<uint8_t> Blob;
+ EncodeLogArgs(fmt::make_format_args(P), Blob);
+
+ // Plain `{}` against the pre-rendered string works fine.
+ CHECK_EQ(FormatLogArgs("{}", Blob.data(), Blob.size()), "(3,4)");
+
+ // A type-specific spec against the decoded string surfaces a soft fmt
+ // error (simulating e.g. chrono `{:%Y-%m-%d}` against a time_point).
+ const std::string BadOut = FormatLogArgs("{:d}", Blob.data(), Blob.size());
+ CHECK_MESSAGE(BadOut.find("fmt error") != std::string::npos,
+ "type-specific spec against flattened string should soft-fail, got: ",
+ BadOut);
+}
+
+TEST_SUITE_END();
+
+} // namespace zen::logging::tests
+
+# endif // ZEN_WITH_TESTS
+
+#endif // ZEN_WITH_TRACE
diff --git a/src/zencore/logging/tracesink.cpp b/src/zencore/logging/tracesink.cpp
deleted file mode 100644
index 8a6f4e40c..000000000
--- a/src/zencore/logging/tracesink.cpp
+++ /dev/null
@@ -1,92 +0,0 @@
-
-// Copyright Epic Games, Inc. All Rights Reserved.
-
-#include <zencore/logbase.h>
-#include <zencore/logging/tracesink.h>
-#include <zencore/string.h>
-#include <zencore/timer.h>
-#include <zencore/trace.h>
-
-#if ZEN_WITH_TRACE
-
-namespace zen::logging {
-
-UE_TRACE_CHANNEL_DEFINE(LogChannel)
-
-UE_TRACE_EVENT_BEGIN(Logging, LogCategory, NoSync | Important)
- UE_TRACE_EVENT_FIELD(const void*, CategoryPointer)
- UE_TRACE_EVENT_FIELD(uint8_t, DefaultVerbosity)
- UE_TRACE_EVENT_FIELD(UE::Trace::AnsiString, Name)
-UE_TRACE_EVENT_END()
-
-UE_TRACE_EVENT_BEGIN(Logging, LogMessageSpec, NoSync | Important)
- UE_TRACE_EVENT_FIELD(const void*, LogPoint)
- UE_TRACE_EVENT_FIELD(const void*, CategoryPointer)
- UE_TRACE_EVENT_FIELD(int32_t, Line)
- UE_TRACE_EVENT_FIELD(uint8_t, Verbosity)
- UE_TRACE_EVENT_FIELD(UE::Trace::AnsiString, FileName)
- UE_TRACE_EVENT_FIELD(UE::Trace::AnsiString, FormatString)
-UE_TRACE_EVENT_END()
-
-UE_TRACE_EVENT_BEGIN(Logging, LogMessage, NoSync)
- UE_TRACE_EVENT_FIELD(const void*, LogPoint)
- UE_TRACE_EVENT_FIELD(uint64_t, Cycle)
- UE_TRACE_EVENT_FIELD(uint8_t[], FormatArgs)
-UE_TRACE_EVENT_END()
-
-void
-TraceLogCategory(const logging::Logger* Category, const char* Name, logging::LogLevel DefaultVerbosity)
-{
- uint16_t NameLen = uint16_t(strlen(Name));
- UE_TRACE_LOG(Logging, LogCategory, LogChannel, NameLen * sizeof(ANSICHAR))
- << LogCategory.CategoryPointer(Category) << LogCategory.DefaultVerbosity(uint8_t(DefaultVerbosity))
- << LogCategory.Name(Name, NameLen);
-}
-
-void
-TraceLogMessageSpec(const void* LogPoint,
- const logging::Logger* Category,
- logging::LogLevel Verbosity,
- const std::string_view File,
- int32_t Line,
- const std::string_view Format)
-{
- uint16_t FileNameLen = uint16_t(File.size());
- uint16_t FormatStringLen = uint16_t(Format.size());
- uint32_t DataSize = (FileNameLen * sizeof(ANSICHAR)) + (FormatStringLen * sizeof(ANSICHAR));
- UE_TRACE_LOG(Logging, LogMessageSpec, LogChannel, DataSize)
- << LogMessageSpec.LogPoint(LogPoint) << LogMessageSpec.CategoryPointer(Category) << LogMessageSpec.Line(Line)
- << LogMessageSpec.Verbosity(uint8_t(Verbosity)) << LogMessageSpec.FileName(File.data(), FileNameLen)
- << LogMessageSpec.FormatString(Format.data(), FormatStringLen);
-}
-
-void
-TraceLogMessageInternal(const void* LogPoint, int32_t EncodedFormatArgsSize, const uint8_t* EncodedFormatArgs)
-{
- UE_TRACE_LOG(Logging, LogMessage, LogChannel) << LogMessage.LogPoint(LogPoint) << LogMessage.Cycle(GetHifreqTimerValue())
- << LogMessage.FormatArgs(EncodedFormatArgs, EncodedFormatArgsSize);
-}
-
-//////////////////////////////////////////////////////////////////////////
-
-void
-TraceSink::Log(const LogMessage& Msg)
-{
- ZEN_UNUSED(Msg);
-}
-
-void
-TraceSink::Flush()
-{
-}
-
-void
-TraceSink::SetFormatter(std::unique_ptr<Formatter> /*InFormatter*/)
-{
- // This sink doesn't use a formatter since it just forwards the raw format
- // args to the trace system
-}
-
-} // namespace zen::logging
-
-#endif
diff --git a/src/zencore/memory/memory.cpp b/src/zencore/memory/memory.cpp
index 9e19c5db7..8dbb04e64 100644
--- a/src/zencore/memory/memory.cpp
+++ b/src/zencore/memory/memory.cpp
@@ -44,10 +44,10 @@ InitGMalloc()
// when using sanitizers, we should use the default/ansi allocator
#if ZEN_OVERRIDE_NEW_DELETE
-# if ZEN_RPMALLOC_ENABLED
+# if ZEN_MIMALLOC_ENABLED
if (Malloc == MallocImpl::None)
{
- Malloc = MallocImpl::Rpmalloc;
+ Malloc = MallocImpl::Mimalloc;
}
# endif
#endif
diff --git a/src/zencore/process.cpp b/src/zencore/process.cpp
index 9cbbfa56a..5d37c3715 100644
--- a/src/zencore/process.cpp
+++ b/src/zencore/process.cpp
@@ -28,6 +28,7 @@ ZEN_THIRD_PARTY_INCLUDES_START
# include <pthread.h>
# include <signal.h>
# include <sys/file.h>
+# include <sys/resource.h>
# include <sys/sem.h>
# include <sys/stat.h>
# include <sys/syscall.h>
@@ -37,7 +38,9 @@ ZEN_THIRD_PARTY_INCLUDES_START
#endif
#if ZEN_PLATFORM_MAC
+# include <crt_externs.h>
# include <libproc.h>
+# include <spawn.h>
# include <sys/types.h>
# include <sys/sysctl.h>
#endif
@@ -135,8 +138,68 @@ IsZombieProcess(int pid, std::error_code& OutEc)
}
return false;
}
+
+static char**
+GetEnviron()
+{
+ return *_NSGetEnviron();
+}
#endif // ZEN_PLATFORM_MAC
+#if ZEN_PLATFORM_LINUX
+static char**
+GetEnviron()
+{
+ return environ;
+}
+#endif // ZEN_PLATFORM_LINUX
+
+#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
+// Holds a null-terminated envp array built by merging the current process environment with
+// a set of overrides. When Overrides is empty, Data points directly to environ (no allocation).
+// Must outlive any posix_spawn / execve call that receives Data.
+struct EnvpHolder
+{
+ char** Data = GetEnviron();
+
+ explicit EnvpHolder(const std::vector<std::pair<std::string, std::string>>& Overrides)
+ {
+ if (Overrides.empty())
+ {
+ return;
+ }
+ std::map<std::string, std::string> EnvMap;
+ for (char** E = GetEnviron(); *E; ++E)
+ {
+ std::string_view Entry(*E);
+ const size_t EqPos = Entry.find('=');
+ if (EqPos != std::string_view::npos)
+ {
+ EnvMap[std::string(Entry.substr(0, EqPos))] = std::string(Entry.substr(EqPos + 1));
+ }
+ }
+ for (const auto& [Key, Value] : Overrides)
+ {
+ EnvMap[Key] = Value;
+ }
+ for (const auto& [Key, Value] : EnvMap)
+ {
+ m_Strings.push_back(Key + "=" + Value);
+ }
+ for (std::string& S : m_Strings)
+ {
+ m_Ptrs.push_back(S.data());
+ }
+ m_Ptrs.push_back(nullptr);
+ Data = m_Ptrs.data();
+ }
+
+private:
+ std::vector<std::string> m_Strings;
+ std::vector<char*> m_Ptrs;
+};
+#endif // ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
+
//////////////////////////////////////////////////////////////////////////
// Pipe creation for child process stdout capture
@@ -209,6 +272,79 @@ CreateStdoutPipe(StdoutPipeHandles& OutPipe)
return true;
}
+StdinPipeHandles::~StdinPipeHandles()
+{
+ Close();
+}
+
+StdinPipeHandles::StdinPipeHandles(StdinPipeHandles&& Other) noexcept
+: ReadHandle(std::exchange(Other.ReadHandle, nullptr))
+, WriteHandle(std::exchange(Other.WriteHandle, nullptr))
+{
+}
+
+StdinPipeHandles&
+StdinPipeHandles::operator=(StdinPipeHandles&& Other) noexcept
+{
+ if (this != &Other)
+ {
+ Close();
+ ReadHandle = std::exchange(Other.ReadHandle, nullptr);
+ WriteHandle = std::exchange(Other.WriteHandle, nullptr);
+ }
+ return *this;
+}
+
+void
+StdinPipeHandles::CloseReadEnd()
+{
+ if (ReadHandle)
+ {
+ CloseHandle(ReadHandle);
+ ReadHandle = nullptr;
+ }
+}
+
+void
+StdinPipeHandles::CloseWriteEnd()
+{
+ if (WriteHandle)
+ {
+ CloseHandle(WriteHandle);
+ WriteHandle = nullptr;
+ }
+}
+
+void
+StdinPipeHandles::Close()
+{
+ CloseReadEnd();
+ CloseWriteEnd();
+}
+
+bool
+CreateStdinPipe(StdinPipeHandles& OutPipe)
+{
+ SECURITY_ATTRIBUTES Sa;
+ Sa.nLength = sizeof(Sa);
+ Sa.lpSecurityDescriptor = nullptr;
+ Sa.bInheritHandle = TRUE;
+
+ HANDLE ReadHandle = nullptr;
+ HANDLE WriteHandle = nullptr;
+ if (!::CreatePipe(&ReadHandle, &WriteHandle, &Sa, 0))
+ {
+ return false;
+ }
+
+ // The write end should not be inherited by the child
+ SetHandleInformation(WriteHandle, HANDLE_FLAG_INHERIT, 0);
+
+ OutPipe.ReadHandle = ReadHandle;
+ OutPipe.WriteHandle = WriteHandle;
+ return true;
+}
+
#else
StdoutPipeHandles::~StdoutPipeHandles()
@@ -271,6 +407,72 @@ CreateStdoutPipe(StdoutPipeHandles& OutPipe)
return true;
}
+StdinPipeHandles::~StdinPipeHandles()
+{
+ Close();
+}
+
+StdinPipeHandles::StdinPipeHandles(StdinPipeHandles&& Other) noexcept
+: ReadFd(std::exchange(Other.ReadFd, -1))
+, WriteFd(std::exchange(Other.WriteFd, -1))
+{
+}
+
+StdinPipeHandles&
+StdinPipeHandles::operator=(StdinPipeHandles&& Other) noexcept
+{
+ if (this != &Other)
+ {
+ Close();
+ ReadFd = std::exchange(Other.ReadFd, -1);
+ WriteFd = std::exchange(Other.WriteFd, -1);
+ }
+ return *this;
+}
+
+void
+StdinPipeHandles::CloseReadEnd()
+{
+ if (ReadFd >= 0)
+ {
+ close(ReadFd);
+ ReadFd = -1;
+ }
+}
+
+void
+StdinPipeHandles::CloseWriteEnd()
+{
+ if (WriteFd >= 0)
+ {
+ close(WriteFd);
+ WriteFd = -1;
+ }
+}
+
+void
+StdinPipeHandles::Close()
+{
+ CloseReadEnd();
+ CloseWriteEnd();
+}
+
+bool
+CreateStdinPipe(StdinPipeHandles& OutPipe)
+{
+ int Fds[2];
+ if (pipe(Fds) != 0)
+ {
+ return false;
+ }
+ OutPipe.ReadFd = Fds[0];
+ OutPipe.WriteFd = Fds[1];
+
+ // Set close-on-exec on the write end so the child doesn't inherit it
+ fcntl(OutPipe.WriteFd, F_SETFD, FD_CLOEXEC);
+ return true;
+}
+
#endif
//////////////////////////////////////////////////////////////////////////
@@ -444,7 +646,7 @@ ProcessHandle::Kill()
std::error_code Ec;
if (!Wait(5000, Ec))
{
- // Graceful shutdown timed out — force-kill
+ // Graceful shutdown timed out - force-kill
kill(pid_t(m_Pid), SIGKILL);
Wait(1000, Ec);
}
@@ -652,47 +854,108 @@ ProcessHandle::WaitExitCode()
//////////////////////////////////////////////////////////////////////////
#if !ZEN_PLATFORM_WINDOWS || ZEN_WITH_TESTS
+// Splits an in-process command-line string into argv, in place. Double quotes
+// suppress space-splitting and are themselves stripped out (shell-style), so
+// `foo "a b" c` -> {"foo", "a b", "c"} and `--k="a b"` -> {"--k=a b"}.
+// Quotes do not have to wrap the whole token; pairs anywhere in a token are
+// removed. There is no escape mechanism — `\"` is not recognised.
static void
BuildArgV(std::vector<char*>& Out, char* CommandLine)
{
- char* Cursor = CommandLine;
+ char* Read = CommandLine;
while (true)
{
- // Skip leading whitespace
- for (; *Cursor == ' '; ++Cursor)
+ // Skip leading whitespace between tokens
+ for (; *Read == ' '; ++Read)
;
- // Check for nullp terminator
- if (*Cursor == '\0')
+ if (*Read == '\0')
{
break;
}
- Out.push_back(Cursor);
+ // Compact in place: Write trails Read, omitting quote chars
+ char* Write = Read;
+ Out.push_back(Write);
- // Extract word
- int QuoteCount = 0;
- do
+ bool InQuotes = false;
+ while (*Read != '\0')
{
- QuoteCount += (*Cursor == '\"');
- if (*Cursor == ' ' && !(QuoteCount & 1))
+ if (*Read == '\"')
+ {
+ InQuotes = !InQuotes;
+ ++Read;
+ continue;
+ }
+ if (*Read == ' ' && !InQuotes)
{
break;
}
- ++Cursor;
- } while (*Cursor != '\0');
+ *Write++ = *Read++;
+ }
- if (*Cursor == '\0')
+ const bool AtEnd = (*Read == '\0');
+ *Write = '\0';
+ if (AtEnd)
{
break;
}
-
- *Cursor = '\0';
- ++Cursor;
+ ++Read;
}
}
+
#endif // !WINDOWS || TESTS
+std::string
+GetRawCommandLine()
+{
+#if ZEN_PLATFORM_WINDOWS
+ LPWSTR Raw = ::GetCommandLineW();
+ if (Raw == nullptr)
+ {
+ return {};
+ }
+ return WideToUtf8(Raw);
+#else
+ return {};
+#endif
+}
+
+std::string
+BuildCommandLine(std::span<const std::string> Argv)
+{
+ constexpr AsciiSet QuoteChars = " \t\"";
+
+ std::string Result;
+ for (size_t I = 0; I < Argv.size(); ++I)
+ {
+ if (I > 0)
+ {
+ Result += ' ';
+ }
+
+ const std::string& Arg = Argv[I];
+ const bool NeedsQuotes = Arg.empty() || AsciiSet::HasAny(Arg.c_str(), QuoteChars);
+ if (!NeedsQuotes)
+ {
+ Result += Arg;
+ continue;
+ }
+
+ Result += '"';
+ for (char Ch : Arg)
+ {
+ if (Ch == '"')
+ {
+ Result += '\\';
+ }
+ Result += Ch;
+ }
+ Result += '"';
+ }
+ return Result;
+}
+
#if ZEN_PLATFORM_WINDOWS
static CreateProcResult
CreateProcNormal(const std::filesystem::path& Executable, std::string_view CommandLine, const CreateProcOptions& Options)
@@ -766,10 +1029,14 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
{
CreationFlags |= CREATE_NO_WINDOW;
}
- if (Options.Flags & CreateProcOptions::Flag_Windows_NewProcessGroup)
+ if (Options.Flags & CreateProcOptions::Flag_NewProcessGroup)
{
CreationFlags |= CREATE_NEW_PROCESS_GROUP;
}
+ if (Options.Flags & CreateProcOptions::Flag_BelowNormalPriority)
+ {
+ CreationFlags |= BELOW_NORMAL_PRIORITY_CLASS;
+ }
if (AssignToJob)
{
CreationFlags |= CREATE_SUSPENDED;
@@ -784,19 +1051,25 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
ExtendableWideStringBuilder<256> CommandLineZ;
CommandLineZ << CommandLine;
- bool DuplicatedStdErr = false;
+ bool DuplicatedStdErr = false;
+ bool CreatedStdOutFile = false;
+ bool UseStdHandles = false;
+
+ if (Options.StdinPipe != nullptr && Options.StdinPipe->ReadHandle != nullptr)
+ {
+ StartupInfo.hStdInput = (HANDLE)Options.StdinPipe->ReadHandle;
+ UseStdHandles = true;
+ }
if (Options.StdoutPipe != nullptr && Options.StdoutPipe->WriteHandle != nullptr)
{
- StartupInfo.hStdInput = nullptr;
StartupInfo.hStdOutput = (HANDLE)Options.StdoutPipe->WriteHandle;
+ UseStdHandles = true;
if (Options.StderrPipe != nullptr && Options.StderrPipe->WriteHandle != nullptr)
{
// Use separate pipe for stderr
StartupInfo.hStdError = (HANDLE)Options.StderrPipe->WriteHandle;
- StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
- InheritHandles = true;
}
else
{
@@ -812,8 +1085,6 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
if (DupSuccess)
{
DuplicatedStdErr = true;
- StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
- InheritHandles = true;
}
}
}
@@ -824,7 +1095,6 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
sa.lpSecurityDescriptor = nullptr;
sa.bInheritHandle = TRUE;
- StartupInfo.hStdInput = nullptr;
StartupInfo.hStdOutput = CreateFileW(Options.StdoutFile.c_str(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
@@ -833,25 +1103,54 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
FILE_ATTRIBUTE_NORMAL,
nullptr);
- const BOOL Success = DuplicateHandle(GetCurrentProcess(),
- StartupInfo.hStdOutput,
- GetCurrentProcess(),
- &StartupInfo.hStdError,
- 0,
- TRUE,
- DUPLICATE_SAME_ACCESS);
+ if (StartupInfo.hStdOutput != INVALID_HANDLE_VALUE)
+ {
+ CreatedStdOutFile = true;
+ UseStdHandles = true;
+
+ const BOOL Success = DuplicateHandle(GetCurrentProcess(),
+ StartupInfo.hStdOutput,
+ GetCurrentProcess(),
+ &StartupInfo.hStdError,
+ 0,
+ TRUE,
+ DUPLICATE_SAME_ACCESS);
+
+ if (Success)
+ {
+ DuplicatedStdErr = true;
+ }
+ else
+ {
+ CloseHandle(StartupInfo.hStdOutput);
+ StartupInfo.hStdOutput = 0;
+ CreatedStdOutFile = false;
+ UseStdHandles = (Options.StdinPipe != nullptr && Options.StdinPipe->ReadHandle != nullptr);
+ }
+ }
+ }
- if (Success)
+ if (UseStdHandles)
+ {
+ // When STARTF_USESTDHANDLES is set, Windows requires all three handles to be
+ // specified. Fall back to the parent's current std handles for any that the
+ // caller didn't supply. This is best-effort: GetStdHandle may return handles
+ // that are not inheritable in a headless parent, in which case the child will
+ // see closed handles on those streams.
+ if (StartupInfo.hStdInput == nullptr)
{
- DuplicatedStdErr = true;
- StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
- InheritHandles = true;
+ StartupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
}
- else
+ if (StartupInfo.hStdOutput == nullptr)
{
- CloseHandle(StartupInfo.hStdOutput);
- StartupInfo.hStdOutput = 0;
+ StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
}
+ if (StartupInfo.hStdError == nullptr)
+ {
+ StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);
+ }
+ StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
+ InheritHandles = true;
}
BOOL Success = CreateProcessW(Executable.c_str(),
@@ -873,7 +1172,7 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
CloseHandle(StartupInfo.hStdError);
}
// Only close hStdOutput if it was a file handle we created (not a pipe handle owned by caller)
- if (Options.StdoutPipe == nullptr || Options.StdoutPipe->WriteHandle == nullptr)
+ if (CreatedStdOutFile)
{
CloseHandle(StartupInfo.hStdOutput);
}
@@ -980,6 +1279,10 @@ CreateProcUnelevated(const std::filesystem::path& Executable, std::string_view C
{
CreateProcFlags |= CREATE_NO_WINDOW;
}
+ if (Options.Flags & CreateProcOptions::Flag_BelowNormalPriority)
+ {
+ CreateProcFlags |= BELOW_NORMAL_PRIORITY_CLASS;
+ }
if (AssignToJob)
{
CreateProcFlags |= CREATE_SUSPENDED;
@@ -1070,23 +1373,37 @@ CreateProc(const std::filesystem::path& Executable, std::string_view CommandLine
}
return CreateProcNormal(Executable, CommandLine, Options);
-#else
+#elif ZEN_PLATFORM_LINUX
+ // vfork uses CLONE_VM|CLONE_VFORK: the child shares the parent's address space and the
+ // parent is suspended until the child calls exec or _exit. This avoids page-table duplication
+ // and the ENOMEM that fork() produces on systems with strict overcommit (vm.overcommit_memory=2).
+ // All child-side setup uses only syscalls that do not modify user-space memory.
+ // Environment overrides are merged into envp before vfork so that setenv() is never called
+ // from the child (which would corrupt the shared address space).
std::vector<char*> ArgV;
std::string CommandLineZ(CommandLine);
BuildArgV(ArgV, CommandLineZ.data());
ArgV.push_back(nullptr);
- int ChildPid = fork();
+ EnvpHolder Envp(Options.Environment);
+
+ int ChildPid = vfork();
if (ChildPid < 0)
{
- ThrowLastError("Failed to fork a new child process");
+ ThrowLastError("Failed to vfork a new child process");
}
else if (ChildPid == 0)
{
if (Options.WorkingDirectory != nullptr)
{
- int Result = chdir(Options.WorkingDirectory->c_str());
- ZEN_UNUSED(Result);
+ chdir(Options.WorkingDirectory->c_str());
+ }
+
+ if (Options.StdinPipe != nullptr && Options.StdinPipe->ReadFd >= 0)
+ {
+ dup2(Options.StdinPipe->ReadFd, STDIN_FILENO);
+ close(Options.StdinPipe->ReadFd);
+ // WriteFd has FD_CLOEXEC so it's auto-closed on exec
}
if (Options.StdoutPipe != nullptr && Options.StdoutPipe->WriteFd >= 0)
@@ -1118,23 +1435,118 @@ CreateProc(const std::filesystem::path& Executable, std::string_view CommandLine
}
}
- if (Options.ProcessGroupId > 0)
+ if (Options.Flags & CreateProcOptions::Flag_NewProcessGroup)
+ {
+ setpgid(0, 0);
+ }
+ else if (Options.ProcessGroupId > 0)
{
setpgid(0, Options.ProcessGroupId);
}
- for (const auto& [Key, Value] : Options.Environment)
+ execve(Executable.c_str(), ArgV.data(), Envp.Data);
+ _exit(127);
+ }
+
+ if (Options.Flags & CreateProcOptions::Flag_BelowNormalPriority)
+ {
+ setpriority(PRIO_PROCESS, ChildPid, 5);
+ }
+
+ return ChildPid;
+#else // macOS
+ std::vector<char*> ArgV;
+ std::string CommandLineZ(CommandLine);
+ BuildArgV(ArgV, CommandLineZ.data());
+ ArgV.push_back(nullptr);
+
+ posix_spawn_file_actions_t FileActions;
+ posix_spawnattr_t Attr;
+
+ int Err = posix_spawn_file_actions_init(&FileActions);
+ if (Err != 0)
+ {
+ ThrowSystemError(Err, "posix_spawn_file_actions_init failed");
+ }
+ auto FileActionsGuard = MakeGuard([&] { posix_spawn_file_actions_destroy(&FileActions); });
+
+ Err = posix_spawnattr_init(&Attr);
+ if (Err != 0)
+ {
+ ThrowSystemError(Err, "posix_spawnattr_init failed");
+ }
+ auto AttrGuard = MakeGuard([&] { posix_spawnattr_destroy(&Attr); });
+
+ if (Options.WorkingDirectory != nullptr)
+ {
+ Err = posix_spawn_file_actions_addchdir_np(&FileActions, Options.WorkingDirectory->c_str());
+ if (Err != 0)
{
- setenv(Key.c_str(), Value.c_str(), 1);
+ ThrowSystemError(Err, "posix_spawn_file_actions_addchdir_np failed");
}
+ }
- if (execv(Executable.c_str(), ArgV.data()) < 0)
+ if (Options.StdinPipe != nullptr && Options.StdinPipe->ReadFd >= 0)
+ {
+ const int StdinReadFd = Options.StdinPipe->ReadFd;
+ ZEN_ASSERT(StdinReadFd > STDERR_FILENO);
+ posix_spawn_file_actions_adddup2(&FileActions, StdinReadFd, STDIN_FILENO);
+ posix_spawn_file_actions_addclose(&FileActions, StdinReadFd);
+ // WriteFd has FD_CLOEXEC so it's auto-closed on exec
+ }
+
+ if (Options.StdoutPipe != nullptr && Options.StdoutPipe->WriteFd >= 0)
+ {
+ const int StdoutWriteFd = Options.StdoutPipe->WriteFd;
+ ZEN_ASSERT(StdoutWriteFd > STDERR_FILENO);
+ posix_spawn_file_actions_adddup2(&FileActions, StdoutWriteFd, STDOUT_FILENO);
+
+ if (Options.StderrPipe != nullptr && Options.StderrPipe->WriteFd >= 0)
{
- ThrowLastError("Failed to exec() a new process image");
+ const int StderrWriteFd = Options.StderrPipe->WriteFd;
+ ZEN_ASSERT(StderrWriteFd > STDERR_FILENO && StderrWriteFd != StdoutWriteFd);
+ posix_spawn_file_actions_adddup2(&FileActions, StderrWriteFd, STDERR_FILENO);
+ posix_spawn_file_actions_addclose(&FileActions, StderrWriteFd);
}
+ else
+ {
+ posix_spawn_file_actions_adddup2(&FileActions, StdoutWriteFd, STDERR_FILENO);
+ }
+
+ posix_spawn_file_actions_addclose(&FileActions, StdoutWriteFd);
+ }
+ else if (!Options.StdoutFile.empty())
+ {
+ posix_spawn_file_actions_addopen(&FileActions, STDOUT_FILENO, Options.StdoutFile.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ posix_spawn_file_actions_adddup2(&FileActions, STDOUT_FILENO, STDERR_FILENO);
}
- return ChildPid;
+ if (Options.Flags & CreateProcOptions::Flag_NewProcessGroup)
+ {
+ posix_spawnattr_setflags(&Attr, POSIX_SPAWN_SETPGROUP);
+ posix_spawnattr_setpgroup(&Attr, 0);
+ }
+ else if (Options.ProcessGroupId > 0)
+ {
+ posix_spawnattr_setflags(&Attr, POSIX_SPAWN_SETPGROUP);
+ posix_spawnattr_setpgroup(&Attr, Options.ProcessGroupId);
+ }
+
+ EnvpHolder Envp(Options.Environment);
+
+ pid_t ChildPid = 0;
+ Err = posix_spawn(&ChildPid, Executable.c_str(), &FileActions, &Attr, ArgV.data(), Envp.Data);
+ if (Err != 0)
+ {
+ ThrowSystemError(Err, "Failed to posix_spawn a new child process");
+ }
+
+ if (Options.Flags & CreateProcOptions::Flag_BelowNormalPriority)
+ {
+ setpriority(PRIO_PROCESS, ChildPid, 5);
+ }
+
+ return int(ChildPid);
#endif
}
@@ -1590,7 +2002,7 @@ GetProcessCommandLine(int Pid, std::error_code& OutEc)
++p; // skip null terminator of argv[0]
}
- // Build result: remaining entries joined by spaces (inter-arg nulls → spaces)
+ // Build result: remaining entries joined by spaces (inter-arg nulls -> spaces)
std::string Result;
Result.reserve(static_cast<size_t>(End - p));
for (const char* q = p; q < End; ++q)
@@ -1682,9 +2094,41 @@ GetProcessCommandLine(int Pid, std::error_code& OutEc)
#endif
}
+#if ZEN_PLATFORM_WINDOWS
+static const wchar_t*
+StripExtendedLengthPrefix(const wchar_t* S)
+{
+ // "\\?\C:\foo" -> "C:\foo"; "\\?\UNC\srv\share" -> "UNC\srv\share".
+ // UNC stripping is asymmetric vs a bare "\\srv\share" form, so this helper only normalizes
+ // the "\\?\" prefix itself. Comparing a "\\?\UNC\..." path to a bare "\\..." path is not
+ // expected for FindProcess inputs (zenserver installs are local paths).
+ if (S[0] == L'\\' && S[1] == L'\\' && S[2] == L'?' && S[3] == L'\\')
+ {
+ return S + 4;
+ }
+ return S;
+}
+#endif
+
+static bool
+IsSameNativePath(const std::filesystem::path& A, const std::filesystem::path& B)
+{
+#if ZEN_PLATFORM_WINDOWS
+ // Windows filesystem is case-insensitive; std::filesystem::path::operator== is not.
+ // CompareStringOrdinal is Microsoft's recommended API for filename/resource comparison:
+ // ordinal (no locale sensitivity) and case-insensitive when bIgnoreCase is TRUE.
+ // Strip "\\?\" extended-length prefix first so paths produced by MakeSafeAbsolutePath
+ // compare equal to paths from GetProcessExecutablePath which do not carry the prefix.
+ return CompareStringOrdinal(StripExtendedLengthPrefix(A.c_str()), -1, StripExtendedLengthPrefix(B.c_str()), -1, TRUE) == CSTR_EQUAL;
+#else
+ return A == B;
+#endif
+}
+
std::error_code
FindProcess(const std::filesystem::path& ExecutableImage, ProcessHandle& OutHandle, bool IncludeSelf)
{
+ const bool MatchFullPath = ExecutableImage.has_parent_path();
#if ZEN_PLATFORM_WINDOWS
HANDLE ProcessSnapshotHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (ProcessSnapshotHandle == INVALID_HANDLE_VALUE)
@@ -1701,29 +2145,41 @@ FindProcess(const std::filesystem::path& ExecutableImage, ProcessHandle& OutHand
{
do
{
- if ((IncludeSelf || (Entry.th32ProcessID != ThisProcessId)) && (ExecutableImage.filename() == Entry.szExeFile))
+ if ((IncludeSelf || (Entry.th32ProcessID != ThisProcessId)) && IsSameNativePath(ExecutableImage.filename(), Entry.szExeFile))
{
- std::error_code Ec;
- std::filesystem::path EntryPath = GetProcessExecutablePath(Entry.th32ProcessID, Ec);
- if (!Ec)
+ HANDLE Handle = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE | PROCESS_QUERY_INFORMATION, FALSE, Entry.th32ProcessID);
+ if (Handle == NULL)
{
- if (EntryPath == ExecutableImage)
+ // Skip processes we can't open (access denied, exited between snapshot and open, etc.)
+ continue;
+ }
+ // Close on all exits from this iteration unless ownership transfers to OutHandle (Handle set to NULL).
+ auto HandleGuard = MakeGuard([&]() {
+ if (Handle != NULL)
+ {
+ CloseHandle(Handle);
+ Handle = NULL;
+ }
+ });
+ DWORD ExitCode = 0;
+ bool Match = false;
+ if (GetExitCodeProcess(Handle, &ExitCode) && ExitCode == STILL_ACTIVE)
+ {
+ // Re-verify executable path post-open so PID reuse between snapshot and open is caught.
+ std::error_code Ec;
+ std::filesystem::path EntryPath = GetProcessExecutablePath(Entry.th32ProcessID, Ec);
+ if (!Ec)
{
- HANDLE Handle =
- OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE | PROCESS_QUERY_INFORMATION, FALSE, Entry.th32ProcessID);
- if (Handle == NULL)
- {
- return MakeErrorCodeFromLastError();
- }
- DWORD ExitCode = 0;
- GetExitCodeProcess(Handle, &ExitCode);
- if (ExitCode == STILL_ACTIVE)
- {
- OutHandle.Initialize((void*)Handle);
- return {};
- }
+ Match = MatchFullPath ? IsSameNativePath(EntryPath, ExecutableImage)
+ : IsSameNativePath(EntryPath.filename(), ExecutableImage.filename());
}
}
+ if (Match)
+ {
+ OutHandle.Initialize((void*)Handle);
+ Handle = NULL;
+ return {};
+ }
}
} while (::Process32Next(ProcessSnapshotHandle, (LPPROCESSENTRY32)&Entry));
return {};
@@ -1773,7 +2229,9 @@ FindProcess(const std::filesystem::path& ExecutableImage, ProcessHandle& OutHand
std::filesystem::path EntryPath = GetProcessExecutablePath(Pid, Ec);
if (!Ec)
{
- if (EntryPath == ExecutableImage)
+ const bool Match = MatchFullPath ? IsSameNativePath(EntryPath, ExecutableImage)
+ : IsSameNativePath(EntryPath.filename(), ExecutableImage.filename());
+ if (Match)
{
if (Processes[ProcIndex].kp_proc.p_stat != SZOMB)
{
@@ -1811,7 +2269,9 @@ FindProcess(const std::filesystem::path& ExecutableImage, ProcessHandle& OutHand
std::filesystem::path EntryPath = GetProcessExecutablePath((int)Pid, Ec);
if (!Ec)
{
- if (EntryPath == ExecutableImage)
+ const bool Match = MatchFullPath ? IsSameNativePath(EntryPath, ExecutableImage)
+ : IsSameNativePath(EntryPath.filename(), ExecutableImage.filename());
+ if (Match)
{
char Status = GetPidStatus(Pid, Ec);
if (!Ec)
@@ -1928,7 +2388,7 @@ GetProcessMetrics(const ProcessHandle& Handle, ProcessMetrics& OutMetrics)
{
Buf[Len] = '\0';
- // Skip past "pid (name) " — find last ')' to handle names containing spaces or parens
+ // Skip past "pid (name) " - find last ')' to handle names containing spaces or parens
const char* P = strrchr(Buf, ')');
if (P)
{
@@ -2076,6 +2536,54 @@ TEST_CASE("FindProcess")
CHECK(!Ec);
CHECK(!Process.IsValid());
}
+ {
+ ProcessHandle Process;
+ std::filesystem::path BareName = GetRunningExecutablePath().filename();
+ std::error_code Ec = FindProcess(BareName, Process, /*IncludeSelf*/ true);
+ CHECK(!Ec);
+ CHECK(Process.IsValid());
+ }
+ {
+ ProcessHandle Process;
+ std::error_code Ec = FindProcess("this-executable-definitely-does-not-exist.xyz", Process, /*IncludeSelf*/ true);
+ CHECK(!Ec);
+ CHECK(!Process.IsValid());
+ }
+ {
+ // Correct filename but wrong directory must not match when a parent path is supplied.
+ std::filesystem::path WrongPath = std::filesystem::path("nonexistent-dir-7f3a9c1e") / GetRunningExecutablePath().filename();
+ ProcessHandle Process;
+ std::error_code Ec = FindProcess(WrongPath, Process, /*IncludeSelf*/ true);
+ CHECK(!Ec);
+ CHECK(!Process.IsValid());
+ }
+# if ZEN_PLATFORM_WINDOWS
+ {
+ // On Windows, filename match is case-insensitive (filesystem is case-insensitive).
+ std::filesystem::path::string_type Upper = GetRunningExecutablePath().filename().native();
+ for (auto& Ch : Upper)
+ {
+ Ch = towupper(Ch);
+ }
+ ProcessHandle Process;
+ std::error_code Ec = FindProcess(std::filesystem::path(Upper), Process, /*IncludeSelf*/ true);
+ CHECK(!Ec);
+ CHECK(Process.IsValid());
+ }
+ {
+ // "\\?\"-prefixed absolute path must still match a running process whose reported path is unprefixed.
+ std::filesystem::path AbsPath = MakeSafeAbsolutePath(GetRunningExecutablePath());
+ std::filesystem::path::string_type Prefixed = AbsPath.native();
+ if (Prefixed.rfind(L"\\\\?\\", 0) != 0)
+ {
+ Prefixed.insert(0, L"\\\\?\\");
+ }
+ ProcessHandle Process;
+ std::error_code Ec = FindProcess(std::filesystem::path(Prefixed), Process, /*IncludeSelf*/ true);
+ CHECK(!Ec);
+ CHECK(Process.IsValid());
+ }
+# endif
}
TEST_CASE("GetProcessMetrics")
@@ -2098,52 +2606,49 @@ TEST_CASE("GetProcessMetrics")
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"},
+ struct Case
+ {
+ std::initializer_list<const char*> Expected;
+ const char* Input;
+ };
+ const Case Cases[] = {
+ {{}, ""},
+ {{}, " "},
+ {{"one"}, "one"},
+ {{"one"}, " one"},
+ {{"one"}, "one "},
+ {{"one", "two"}, "one two"},
+ {{"one", "two"}, " one two"},
+ {{"one", "two"}, "one two "},
+ {{"one", "two", "three"}, "one two three"},
+ {{"one", "two", "three", "four", "five"}, "one two three four five"},
+
+ // Quotes are stripped (shell-style) and suppress space-splitting
+ {{"one", "two", "three"}, "\"one\" two \"three\""},
+ {{"hello world"}, "\"hello world\""},
+ {{"--key=hello world"}, "--key=\"hello world\""},
+ {{"--key=hello world"}, "\"--key=hello world\""},
+ {{"a b", "c d"}, "\"a b\" \"c d\""},
+ {{"abc"}, "a\"b\"c"},
+ {{""}, "\"\""},
+ {{"foo", "bar baz", "qux"}, "foo \"bar baz\" qux"},
};
for (const auto& Case : Cases)
{
std::vector<char*> OutArgs;
- StringBuilder<64> Mutable;
+ StringBuilder<128> Mutable;
Mutable << Case.Input;
BuildArgV(OutArgs, Mutable.Data());
- CHECK_EQ(OutArgs.size(), Case.WordCount);
+ REQUIRE_EQ(OutArgs.size(), Case.Expected.size());
- for (int i = 0, n = int(OutArgs.size()); i < n; ++i)
+ size_t i = 0;
+ for (const char* Truth : Case.Expected)
{
- 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_MESSAGE(std::string_view(OutArgs[i]) == std::string_view(Truth),
+ fmt::format("input='{}' arg[{}]='{}' expected='{}'", Case.Input, i, OutArgs[i], Truth));
+ ++i;
}
}
}
diff --git a/src/zencore/refcount.cpp b/src/zencore/refcount.cpp
index f19afe715..674b154e0 100644
--- a/src/zencore/refcount.cpp
+++ b/src/zencore/refcount.cpp
@@ -35,29 +35,29 @@ refcount_forcelink()
TEST_SUITE_BEGIN("core.refcount");
-TEST_CASE("RefPtr")
+TEST_CASE("Ref")
{
- RefPtr<TestRefClass> Ref;
- Ref = new TestRefClass;
+ Ref<TestRefClass> RefA;
+ RefA = new TestRefClass;
bool IsDestroyed = false;
- Ref->OnDestroy = [&] { IsDestroyed = true; };
+ RefA->OnDestroy = [&] { IsDestroyed = true; };
CHECK(IsDestroyed == false);
- CHECK(Ref->RefCount() == 1);
+ CHECK(RefA->RefCount() == 1);
- RefPtr<TestRefClass> Ref2;
- Ref2 = Ref;
+ Ref<TestRefClass> RefB;
+ RefB = RefA;
CHECK(IsDestroyed == false);
- CHECK(Ref->RefCount() == 2);
+ CHECK(RefA->RefCount() == 2);
- RefPtr<TestRefClass> Ref3;
- Ref2 = Ref3;
+ Ref<TestRefClass> RefC;
+ RefB = RefC;
CHECK(IsDestroyed == false);
- CHECK(Ref->RefCount() == 1);
- Ref = Ref3;
+ CHECK(RefA->RefCount() == 1);
+ RefA = RefC;
CHECK(IsDestroyed == true);
}
diff --git a/src/zencore/sentryintegration.cpp b/src/zencore/sentryintegration.cpp
index 8491bef64..61b735594 100644
--- a/src/zencore/sentryintegration.cpp
+++ b/src/zencore/sentryintegration.cpp
@@ -250,7 +250,7 @@ SentryIntegration::Initialize(const Config& Conf, const std::string& CommandLine
if (SentryOptions == nullptr)
{
- // OOM — skip sentry entirely rather than crashing on the subsequent set calls
+ // OOM - skip sentry entirely rather than crashing on the subsequent set calls
m_SentryErrorCode = -1;
m_IsInitialized = true;
return;
@@ -261,14 +261,23 @@ SentryIntegration::Initialize(const Config& Conf, const std::string& CommandLine
sentry_options_set_logger(SentryOptions, SentryLogFunction, this);
sentry_options_set_environment(SentryOptions, Conf.Environment.empty() ? "production" : Conf.Environment.c_str());
- std::string SentryAttachmentsPath = Conf.AttachmentsPath;
- if (!SentryAttachmentsPath.empty())
+ for (const std::filesystem::path& AttachmentPath : Conf.AttachmentPaths)
{
- if (SentryAttachmentsPath.starts_with("\\\\?\\"))
+ if (AttachmentPath.empty())
{
- SentryAttachmentsPath = SentryAttachmentsPath.substr(4);
+ continue;
}
- sentry_options_add_attachment(SentryOptions, SentryAttachmentsPath.c_str());
+# if ZEN_PLATFORM_WINDOWS
+ const std::wstring Wide = AttachmentPath.wstring();
+ const wchar_t* WPath = Wide.c_str();
+ if (Wide.starts_with(L"\\\\?\\"))
+ {
+ WPath += 4;
+ }
+ sentry_options_add_attachmentw(SentryOptions, WPath);
+# else
+ sentry_options_add_attachment(SentryOptions, AttachmentPath.string().c_str());
+# endif
}
sentry_options_set_release(SentryOptions, ZEN_CFG_VERSION);
diff --git a/src/zencore/sharedbuffer.cpp b/src/zencore/sharedbuffer.cpp
index 8dc6d49d8..48730e670 100644
--- a/src/zencore/sharedbuffer.cpp
+++ b/src/zencore/sharedbuffer.cpp
@@ -100,7 +100,7 @@ SharedBuffer::MakeView(MemoryView View, SharedBuffer OuterBuffer)
return OuterBuffer;
}
- IoBufferCore* NewCore = new IoBufferCore(OuterBuffer.m_Buffer, View.GetData(), View.GetSize());
+ IoBufferCore* NewCore = new IoBufferCore(OuterBuffer.m_Buffer.Get(), View.GetData(), View.GetSize());
NewCore->SetIsImmutable(true);
return SharedBuffer(NewCore);
}
diff --git a/src/zencore/string.cpp b/src/zencore/string.cpp
index 358722b0b..34519b83b 100644
--- a/src/zencore/string.cpp
+++ b/src/zencore/string.cpp
@@ -9,6 +9,7 @@
#include <inttypes.h>
#include <math.h>
#include <stdio.h>
+#include <charconv>
#include <exception>
#include <ostream>
#include <stdexcept>
@@ -54,15 +55,23 @@ namespace zen {
bool
ToString(std::span<char> Buffer, uint64_t Num)
{
- snprintf(Buffer.data(), Buffer.size(), "%" PRIu64, Num);
-
+ auto [Ptr, Ec] = std::to_chars(Buffer.data(), Buffer.data() + Buffer.size(), Num);
+ if (Ec != std::errc{} || Ptr == Buffer.data() + Buffer.size())
+ {
+ return false;
+ }
+ *Ptr = '\0';
return true;
}
bool
ToString(std::span<char> Buffer, int64_t Num)
{
- snprintf(Buffer.data(), Buffer.size(), "%" PRId64, Num);
-
+ auto [Ptr, Ec] = std::to_chars(Buffer.data(), Buffer.data() + Buffer.size(), Num);
+ if (Ec != std::errc{} || Ptr == Buffer.data() + Buffer.size())
+ {
+ return false;
+ }
+ *Ptr = '\0';
return true;
}
@@ -381,6 +390,34 @@ NiceNumGeneral(uint64_t Num, std::span<char> Buffer, NicenumFormat Format)
}
size_t
+ThousandsToBuffer(uint64_t Num, std::span<char> Buffer)
+{
+ // Format into a temporary buffer without separators
+ char Tmp[24];
+ int Len = snprintf(Tmp, sizeof(Tmp), "%llu", (unsigned long long)Num);
+
+ // Insert comma separators
+ int SepCount = (Len - 1) / 3;
+ int TotalLen = Len + SepCount;
+ ZEN_ASSERT(TotalLen < (int)Buffer.size());
+
+ int Src = Len - 1;
+ int Dst = TotalLen;
+ Buffer[Dst--] = '\0';
+
+ for (int i = 0; Src >= 0; i++)
+ {
+ if (i > 0 && i % 3 == 0)
+ {
+ Buffer[Dst--] = ',';
+ }
+ Buffer[Dst--] = Tmp[Src--];
+ }
+
+ return TotalLen;
+}
+
+size_t
NiceNumToBuffer(uint64_t Num, std::span<char> Buffer)
{
return NiceNumGeneral(Num, Buffer, kNicenum1024);
@@ -515,6 +552,40 @@ template class StringBuilderImpl<wchar_t>;
//////////////////////////////////////////////////////////////////////////
void
+StringBuilderBase::AppendPaddedInt(int64_t Value, int MinWidth)
+{
+ char Buf[24];
+ char* End = Buf + sizeof(Buf);
+ char* Ptr = End;
+ bool Negative = Value < 0;
+ uint64_t Abs = Negative ? uint64_t(-Value) : uint64_t(Value);
+ do
+ {
+ *--Ptr = '0' + char(Abs % 10);
+ Abs /= 10;
+ } while (Abs > 0);
+ while ((End - Ptr) < MinWidth)
+ {
+ *--Ptr = '0';
+ }
+ if (Negative)
+ {
+ *--Ptr = '-';
+ }
+ AppendRange(Ptr, End);
+}
+
+void
+StringBuilderBase::AppendFill(char C, size_t Count)
+{
+ EnsureCapacity(Count);
+ std::memset(m_CurPos, C, Count);
+ m_CurPos += Count;
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+void
UrlDecode(std::string_view InUrl, StringBuilderBase& OutUrl)
{
std::string_view::size_type i = 0;
@@ -1279,6 +1350,81 @@ TEST_CASE("hidesensitivestring")
CHECK_EQ(HideSensitiveString("1234567890123456789"sv), "1234XXXX..."sv);
}
+TEST_CASE("CompactString.default")
+{
+ CompactString S;
+ CHECK(S.IsEmpty());
+ CHECK(S.Size() == 0);
+ CHECK(S.ToView() == std::string_view());
+ CHECK(S.c_str()[0] == '\0');
+}
+
+TEST_CASE("CompactString.empty")
+{
+ CompactString S(std::string_view(""));
+ CHECK(S.IsEmpty());
+ CHECK(S.Size() == 0);
+}
+
+TEST_CASE("CompactString.short")
+{
+ CompactString S(std::string_view("hello"));
+ CHECK(!S.IsEmpty());
+ CHECK(S.Size() == 5);
+ CHECK(S.ToView() == std::string_view("hello"));
+}
+
+TEST_CASE("CompactString.sentinel_boundary")
+{
+ // 254 chars — largest value that fits in the prefix byte
+ std::string Str254(254, 'x');
+ std::string_view View254(Str254);
+ CompactString S(View254);
+ CHECK(S.Size() == 254);
+ CHECK(S.ToView() == View254);
+}
+
+TEST_CASE("CompactString.sentinel_exact")
+{
+ // 255 chars — hits the 0xFF sentinel, falls back to strlen
+ std::string Str255(255, 'y');
+ std::string_view View255(Str255);
+ CompactString S(View255);
+ CHECK(S.Size() == 255);
+ CHECK(S.ToView() == View255);
+}
+
+TEST_CASE("CompactString.long")
+{
+ // Well beyond the sentinel
+ std::string Str512(512, 'z');
+ std::string_view View512(Str512);
+ CompactString S(View512);
+ CHECK(S.Size() == 512);
+ CHECK(S.ToView() == View512);
+}
+
+TEST_CASE("CompactString.move")
+{
+ CompactString A(std::string_view("test"));
+ CompactString B(std::move(A));
+ CHECK(A.IsEmpty());
+ CHECK(B.ToView() == std::string_view("test"));
+
+ CompactString C(std::string_view("first"));
+ CompactString D(std::string_view("second"));
+ D = std::move(C);
+ CHECK(C.IsEmpty());
+ CHECK(D.ToView() == std::string_view("first"));
+}
+
+TEST_CASE("CompactString.implicit_conversion")
+{
+ CompactString S(std::string_view("view"));
+ std::string_view V = S;
+ CHECK(V == std::string_view("view"));
+}
+
TEST_SUITE_END();
void
diff --git a/src/zencore/system.cpp b/src/zencore/system.cpp
index 6909e1a9b..486050d83 100644
--- a/src/zencore/system.cpp
+++ b/src/zencore/system.cpp
@@ -148,6 +148,18 @@ GetSystemMetrics()
return Metrics;
}
+void
+RefreshDynamicSystemMetrics(SystemMetrics& Metrics)
+{
+ MEMORYSTATUSEX MemStatus{.dwLength = sizeof(MEMORYSTATUSEX)};
+ GlobalMemoryStatusEx(&MemStatus);
+
+ Metrics.AvailSystemMemoryMiB = MemStatus.ullAvailPhys / 1024 / 1024;
+ Metrics.AvailVirtualMemoryMiB = MemStatus.ullAvailVirtual / 1024 / 1024;
+ Metrics.AvailPageFileMiB = MemStatus.ullAvailPageFile / 1024 / 1024;
+ Metrics.UptimeSeconds = GetTickCount64() / 1000;
+}
+
std::vector<std::string>
GetLocalIpAddresses()
{
@@ -324,6 +336,51 @@ GetSystemMetrics()
return Metrics;
}
+
+void
+RefreshDynamicSystemMetrics(SystemMetrics& Metrics)
+{
+ long PageSize = sysconf(_SC_PAGE_SIZE);
+ long AvailPages = sysconf(_SC_AVPHYS_PAGES);
+
+ if (AvailPages > 0 && PageSize > 0)
+ {
+ Metrics.AvailSystemMemoryMiB = (AvailPages * PageSize) / 1024 / 1024;
+ Metrics.AvailVirtualMemoryMiB = Metrics.AvailSystemMemoryMiB;
+ }
+
+ if (FILE* UptimeFile = fopen("/proc/uptime", "r"))
+ {
+ double UptimeSec = 0;
+ if (fscanf(UptimeFile, "%lf", &UptimeSec) == 1)
+ {
+ Metrics.UptimeSeconds = static_cast<uint64_t>(UptimeSec);
+ }
+ fclose(UptimeFile);
+ }
+
+ if (FILE* MemInfo = fopen("/proc/meminfo", "r"))
+ {
+ char Line[256];
+ long SwapFree = 0;
+
+ while (fgets(Line, sizeof(Line), MemInfo))
+ {
+ if (strncmp(Line, "SwapFree:", 9) == 0)
+ {
+ sscanf(Line, "SwapFree: %ld kB", &SwapFree);
+ break;
+ }
+ }
+ fclose(MemInfo);
+
+ if (SwapFree > 0)
+ {
+ Metrics.AvailPageFileMiB = SwapFree / 1024;
+ }
+ }
+}
+
#elif ZEN_PLATFORM_MAC
std::string
GetMachineName()
@@ -398,6 +455,36 @@ GetSystemMetrics()
return Metrics;
}
+
+void
+RefreshDynamicSystemMetrics(SystemMetrics& Metrics)
+{
+ vm_size_t PageSize = 0;
+ host_page_size(mach_host_self(), &PageSize);
+
+ vm_statistics64_data_t VmStats;
+ mach_msg_type_number_t InfoCount = sizeof(VmStats) / sizeof(natural_t);
+ host_statistics64(mach_host_self(), HOST_VM_INFO64, (host_info64_t)&VmStats, &InfoCount);
+
+ uint64_t FreeMemory = (uint64_t)(VmStats.free_count + VmStats.inactive_count) * PageSize;
+ Metrics.AvailSystemMemoryMiB = FreeMemory / 1024 / 1024;
+ Metrics.AvailVirtualMemoryMiB = Metrics.VirtualMemoryMiB;
+
+ xsw_usage SwapUsage;
+ size_t Size = sizeof(SwapUsage);
+ sysctlbyname("vm.swapusage", &SwapUsage, &Size, nullptr, 0);
+ Metrics.AvailPageFileMiB = (SwapUsage.xsu_total - SwapUsage.xsu_used) / 1024 / 1024;
+
+ struct timeval BootTime
+ {
+ };
+ Size = sizeof(BootTime);
+ if (sysctlbyname("kern.boottime", &BootTime, &Size, nullptr, 0) == 0)
+ {
+ Metrics.UptimeSeconds = static_cast<uint64_t>(time(nullptr) - BootTime.tv_sec);
+ }
+}
+
#else
# error "Unknown platform"
#endif
@@ -655,11 +742,16 @@ struct SystemMetricsTracker::Impl
std::mutex Mutex;
CpuSampler Sampler;
+ SystemMetrics CachedMetrics;
float CachedCpuPercent = 0.0f;
Clock::time_point NextSampleTime = Clock::now();
std::chrono::milliseconds MinInterval;
- explicit Impl(std::chrono::milliseconds InMinInterval) : MinInterval(InMinInterval) {}
+ explicit Impl(std::chrono::milliseconds InMinInterval) : MinInterval(InMinInterval)
+ {
+ // Capture topology and total memory once; these don't change at runtime
+ CachedMetrics = GetSystemMetrics();
+ }
float SampleCpu()
{
@@ -683,7 +775,8 @@ ExtendedSystemMetrics
SystemMetricsTracker::Query()
{
ExtendedSystemMetrics Metrics;
- static_cast<SystemMetrics&>(Metrics) = GetSystemMetrics();
+ static_cast<SystemMetrics&>(Metrics) = m_Impl->CachedMetrics;
+ RefreshDynamicSystemMetrics(Metrics);
std::lock_guard Lock(m_Impl->Mutex);
Metrics.CpuUsagePercent = m_Impl->SampleCpu();
diff --git a/src/zencore/testing.cpp b/src/zencore/testing.cpp
index 9f88a3365..20a53bff3 100644
--- a/src/zencore/testing.cpp
+++ b/src/zencore/testing.cpp
@@ -39,7 +39,7 @@ PrintCrashCallstack([[maybe_unused]] const char* SignalName)
// Use write() + backtrace_symbols_fd() which are async-signal-safe
write(STDERR_FILENO, "\n*** Caught ", 12);
write(STDERR_FILENO, SignalName, strlen(SignalName));
- write(STDERR_FILENO, " — callstack:\n", 15);
+ write(STDERR_FILENO, " - callstack:\n", 15);
void* Frames[64];
int FrameCount = backtrace(Frames, 64);
@@ -279,6 +279,10 @@ TestRunner::ApplyCommandLine(int Argc, char const* const* Argv, const char* Defa
m_Impl->Session.applyCommandLine(Argc, Argv);
+ // Tests default to Info so that common runs aren't buried in debug/trace output.
+ // Use --debug or --verbose to opt back in when investigating a failure.
+ zen::logging::SetLogLevel(zen::logging::Info);
+
for (int i = 1; i < Argc; ++i)
{
if (Argv[i] == "--debug"sv)
diff --git a/src/zencore/testutils.cpp b/src/zencore/testutils.cpp
index c9908aec8..44446bd40 100644
--- a/src/zencore/testutils.cpp
+++ b/src/zencore/testutils.cpp
@@ -30,11 +30,15 @@ ScopedTemporaryDirectory::ScopedTemporaryDirectory() : m_RootPath(CreateTemporar
{
}
-ScopedTemporaryDirectory::ScopedTemporaryDirectory(std::filesystem::path Directory) : m_RootPath(Directory)
+ScopedTemporaryDirectory::ScopedTemporaryDirectory(std::filesystem::path Directory)
+: m_RootPath(Directory.empty() ? CreateTemporaryDirectory() : Directory)
{
- std::error_code Ec;
- DeleteDirectories(Directory, Ec);
- CreateDirectories(Directory);
+ if (!Directory.empty())
+ {
+ std::error_code Ec;
+ DeleteDirectories(Directory, Ec);
+ CreateDirectories(Directory);
+ }
}
ScopedTemporaryDirectory::~ScopedTemporaryDirectory()
diff --git a/src/zencore/thread.cpp b/src/zencore/thread.cpp
index 067e66c0d..1f4004373 100644
--- a/src/zencore/thread.cpp
+++ b/src/zencore/thread.cpp
@@ -99,7 +99,7 @@ SetNameInternal(DWORD thread_id, const char* name)
#endif
void
-SetCurrentThreadName([[maybe_unused]] std::string_view ThreadName)
+SetCurrentThreadName([[maybe_unused]] std::string_view ThreadName, [[maybe_unused]] int32_t SortHint)
{
constexpr std::string_view::size_type MaxThreadNameLength = 255;
std::string_view LimitedThreadName = ThreadName.substr(0, MaxThreadNameLength);
@@ -108,7 +108,7 @@ SetCurrentThreadName([[maybe_unused]] std::string_view ThreadName)
const int ThreadId = GetCurrentThreadId();
#if ZEN_WITH_TRACE
- trace::ThreadRegister(ThreadNameZ.c_str(), /* system id */ ThreadId, /* sort id */ 0);
+ trace::ThreadRegister(ThreadNameZ.c_str(), /* system id */ ThreadId, /* sort id */ SortHint);
#endif // ZEN_WITH_TRACE
#if ZEN_PLATFORM_WINDOWS
@@ -336,13 +336,17 @@ NamedEvent::NamedEvent(std::string_view EventName)
}
fchmod(Fd, 0666);
- // Use the file path to generate an IPC key
- key_t IpcKey = ftok(EventPath.c_str(), 1);
- if (IpcKey < 0)
+ // Derive the IPC key via fstat() on the fd instead of ftok() on the path:
+ // another owner's destructor can unlink the backing file between our open()
+ // and ftok(), in which case ftok's internal stat() fails with ENOENT. The
+ // formula below matches what glibc/macOS ftok() compute internally.
+ struct stat IpcStat;
+ if (fstat(Fd, &IpcStat) < 0)
{
close(Fd);
- ThrowLastError("Failed to create an SysV IPC key");
+ ThrowLastError("Failed to stat backing file for SysV IPC key");
}
+ const key_t IpcKey = key_t((IpcStat.st_ino & 0xffff) | ((IpcStat.st_dev & 0xff) << 16) | (1 << 24));
// Use the key to create/open the semaphore
int Sem = semget(IpcKey, 1, 0600 | IPC_CREAT);
diff --git a/src/zencore/trace.cpp b/src/zencore/trace.cpp
index 7c195e69f..d6a0b2e92 100644
--- a/src/zencore/trace.cpp
+++ b/src/zencore/trace.cpp
@@ -6,7 +6,9 @@
# include <zencore/zencore.h>
# include <zencore/commandline.h>
# include <zencore/string.h>
+# include <zencore/thread.h>
# include <zencore/logging.h>
+# include <zencore/timer.h>
# define TRACE_IMPLEMENT 1
# undef _WINSOCK_DEPRECATED_NO_WARNINGS
@@ -24,6 +26,21 @@
# include <zencore/memory/fmalloc.h>
# include <zencore/memory/memorytrace.h>
+namespace {
+
+TRACE_EVENT_BEGIN(Misc, RegionBeginWithId, NoSync)
+TRACE_EVENT_FIELD(uint64, CycleAndId)
+TRACE_EVENT_FIELD(UE::Trace::AnsiString, RegionName)
+TRACE_EVENT_FIELD(UE::Trace::AnsiString, Category)
+TRACE_EVENT_END()
+
+TRACE_EVENT_BEGIN(Misc, RegionEndWithId, NoSync)
+TRACE_EVENT_FIELD(uint64, Cycle)
+TRACE_EVENT_FIELD(uint64, RegionId)
+TRACE_EVENT_END()
+
+} // namespace
+
namespace zen {
void
@@ -38,7 +55,7 @@ TraceConfigure(const TraceOptions& Options)
auto ProcessTraceArg = [&](const std::string_view& Arg) {
if (Arg == "default"sv)
{
- ProcessChannelList("cpu,log"sv);
+ ProcessChannelList("cpu,zenlog"sv);
}
else if (Arg == "memory"sv)
{
@@ -53,6 +70,12 @@ TraceConfigure(const TraceOptions& Options)
// memtag actually traces to the memalloc channel
ProcessChannelList("memalloc"sv);
}
+ else if (Arg == "log"sv)
+ {
+ // Upstream UE trace reserves "log" for printf-style Logging.* events, which zen
+ // does not emit. Redirect to the zenlog channel so --trace=log does what users expect.
+ ProcessChannelList("zenlog"sv);
+ }
else
{
// Presume that the argument is a trace channel name
@@ -121,7 +144,7 @@ TraceInit(std::string_view ProgramName)
const char* CommandLineString = "";
# endif
- trace::ThreadRegister("main", /* system id */ 0, /* sort id */ 0);
+ trace::ThreadRegister("main", /* system id */ GetCurrentThreadId(), /* sort id */ -1);
trace::DescribeSession(ProgramName,
# if ZEN_BUILD_DEBUG
trace::Build::Debug,
@@ -163,6 +186,32 @@ TraceStop()
return false;
}
+uint64_t
+TraceBeginRegion(std::string_view RegionName, std::string_view Category)
+{
+ uint64_t RegionId = GetHifreqTimerValue();
+ TRACE_LOG(Misc, RegionBeginWithId, true) << RegionBeginWithId.CycleAndId(RegionId)
+ << RegionBeginWithId.RegionName(RegionName.data(), int32_t(RegionName.size()))
+ << RegionBeginWithId.Category(Category.data(), int32_t(Category.size()));
+ return RegionId;
+}
+
+void
+TraceEndRegion(uint64_t RegionId)
+{
+ TRACE_LOG(Misc, RegionEndWithId, true) << RegionEndWithId.Cycle(GetHifreqTimerValue()) << RegionEndWithId.RegionId(RegionId);
+}
+
+ScopedTraceRegion::ScopedTraceRegion(std::string_view RegionName, std::string_view Category)
+: m_RegionId(TraceBeginRegion(RegionName, Category))
+{
+}
+
+ScopedTraceRegion::~ScopedTraceRegion()
+{
+ TraceEndRegion(m_RegionId);
+}
+
bool
GetTraceOptionsFromCommandline(TraceOptions& OutOptions)
{
diff --git a/src/zencore/workthreadpool.cpp b/src/zencore/workthreadpool.cpp
index 1cb338c66..fb7edb2bd 100644
--- a/src/zencore/workthreadpool.cpp
+++ b/src/zencore/workthreadpool.cpp
@@ -4,6 +4,10 @@
#include <zencore/blockingqueue.h>
#include <zencore/except.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <EASTL/deque.h>
+ZEN_THIRD_PARTY_INCLUDES_END
#include <zencore/logging.h>
#include <zencore/scopeguard.h>
#include <zencore/string.h>
@@ -56,8 +60,8 @@ struct WorkerThreadPool::Impl
std::atomic<size_t> m_WorkerThreadCounter{0};
std::atomic<int> m_FreeWorkerCount{0};
- mutable RwLock m_QueueLock;
- std::deque<Ref<IWork>> m_WorkQueue;
+ mutable RwLock m_QueueLock;
+ eastl::deque<Ref<IWork>> m_WorkQueue;
Impl(int InThreadCount, std::string_view WorkerThreadBaseName)
: m_ThreadCount(InThreadCount)
diff --git a/src/zencore/xmake.lua b/src/zencore/xmake.lua
index fe12c14e8..c5a3ea562 100644
--- a/src/zencore/xmake.lua
+++ b/src/zencore/xmake.lua
@@ -39,6 +39,14 @@ target('zencore')
if is_plat("linux", "macosx") then
add_packages("openssl3") -- required for crypto
end
+
+ if is_plat("linux") and has_config("zenlibsecret") then
+ -- libsecret-1 is pulled from the system package (libsecret-1-dev on
+ -- Debian/Ubuntu, libsecret-devel on Fedora). xmake's package recipe
+ -- resolves include dirs and link flags through pkg-config.
+ add_requires("libsecret")
+ add_packages("libsecret")
+ end
add_packages(
"gsl-lite",
@@ -79,6 +87,11 @@ target('zencore')
add_syslinks("rt")
end
+ if is_plat("macosx") then
+ -- Security.framework for TryProtectData/TryUnprotectData (Keychain)
+ add_frameworks("Security", "CoreFoundation")
+ end
+
if is_plat("windows") then
add_syslinks("Advapi32")
add_syslinks("Dbghelp")