diff options
Diffstat (limited to 'src/zencore')
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") |