aboutsummaryrefslogtreecommitdiff
path: root/src/zencore
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-03-16 10:56:11 +0100
committerGitHub Enterprise <[email protected]>2026-03-16 10:56:11 +0100
commit8c3ba4e8c522d119df3cb48966e36c0eaa80aeb9 (patch)
treecf51b07e097904044b4bf65bc3fe0ad14134074f /src/zencore
parentMerge branch 'sb/no-network' of https://github.ol.epicgames.net/ue-foundation... (diff)
parentEnable cross compilation of Windows targets on Linux (#839) (diff)
downloadzen-sb/no-network.tar.xz
zen-sb/no-network.zip
Merge branch 'main' into sb/no-networksb/no-network
Diffstat (limited to 'src/zencore')
-rw-r--r--src/zencore/callstack.cpp2
-rw-r--r--src/zencore/filesystem.cpp589
-rw-r--r--src/zencore/include/zencore/filesystem.h8
-rw-r--r--src/zencore/include/zencore/logbase.h2
-rw-r--r--src/zencore/include/zencore/process.h9
-rw-r--r--src/zencore/include/zencore/string.h23
-rw-r--r--src/zencore/include/zencore/system.h1
-rw-r--r--src/zencore/memtrack/callstacktrace.cpp154
-rw-r--r--src/zencore/process.cpp56
-rw-r--r--src/zencore/sentryintegration.cpp2
-rw-r--r--src/zencore/string.cpp11
-rw-r--r--src/zencore/system.cpp18
-rw-r--r--src/zencore/testing.cpp13
13 files changed, 739 insertions, 149 deletions
diff --git a/src/zencore/callstack.cpp b/src/zencore/callstack.cpp
index ee0b0625a..a16bb3f13 100644
--- a/src/zencore/callstack.cpp
+++ b/src/zencore/callstack.cpp
@@ -6,7 +6,7 @@
#if ZEN_PLATFORM_WINDOWS
# include <zencore/windows.h>
-# include <Dbghelp.h>
+# include <DbgHelp.h>
#endif
#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
diff --git a/src/zencore/filesystem.cpp b/src/zencore/filesystem.cpp
index 8ed63565c..0d361801f 100644
--- a/src/zencore/filesystem.cpp
+++ b/src/zencore/filesystem.cpp
@@ -17,7 +17,7 @@
#if ZEN_PLATFORM_WINDOWS
# include <zencore/windows.h>
-# include <ShlObj.h>
+# include <shlobj.h>
# pragma comment(lib, "shell32.lib")
# pragma comment(lib, "ole32.lib")
#endif
@@ -32,17 +32,27 @@ ZEN_THIRD_PARTY_INCLUDES_END
#if ZEN_PLATFORM_LINUX
# include <dirent.h>
# include <fcntl.h>
+# include <linux/fs.h>
+# include <linux/magic.h>
+# include <sys/ioctl.h>
# include <sys/resource.h>
# include <sys/mman.h>
# include <sys/stat.h>
+# include <sys/vfs.h>
# include <pwd.h>
# include <unistd.h>
+// XFS_SUPER_MAGIC is not always defined in linux/magic.h
+# ifndef XFS_SUPER_MAGIC
+# define XFS_SUPER_MAGIC 0x58465342
+# endif
#endif
#if ZEN_PLATFORM_MAC
# include <dirent.h>
# include <fcntl.h>
# include <libproc.h>
+# include <sys/attr.h>
+# include <sys/clonefile.h>
# include <sys/resource.h>
# include <sys/mman.h>
# include <sys/stat.h>
@@ -59,6 +69,53 @@ namespace zen {
using namespace std::literals;
+#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
+struct ScopedFd
+{
+ int Fd = -1;
+
+ ScopedFd() = default;
+ explicit ScopedFd(int InFd) : Fd(InFd) {}
+
+ ~ScopedFd()
+ {
+ if (Fd >= 0)
+ {
+ close(Fd);
+ }
+ }
+
+ ScopedFd(const ScopedFd&) = delete;
+ ScopedFd& operator=(const ScopedFd&) = delete;
+
+ ScopedFd(ScopedFd&& Other) noexcept : Fd(Other.Fd) { Other.Fd = -1; }
+
+ ScopedFd& operator=(ScopedFd&& Other) noexcept
+ {
+ if (this != &Other)
+ {
+ if (Fd >= 0)
+ {
+ close(Fd);
+ }
+ Fd = Other.Fd;
+ Other.Fd = -1;
+ }
+ return *this;
+ }
+
+ // Release ownership of the file descriptor, returning it without closing
+ int Release()
+ {
+ int Result = Fd;
+ Fd = -1;
+ return Result;
+ }
+
+ explicit operator bool() const { return Fd >= 0; }
+};
+#endif // ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
+
#if ZEN_PLATFORM_WINDOWS
static bool
@@ -615,6 +672,38 @@ SupportsBlockRefCounting(std::filesystem::path Path)
}
return true;
+#elif ZEN_PLATFORM_LINUX
+ struct statfs Buf;
+ if (statfs(Path.c_str(), &Buf) != 0)
+ {
+ return false;
+ }
+
+ // Btrfs and XFS (when formatted with reflink support) support FICLONE
+ return Buf.f_type == BTRFS_SUPER_MAGIC || Buf.f_type == XFS_SUPER_MAGIC;
+#elif ZEN_PLATFORM_MAC
+ struct attrlist AttrList = {};
+ AttrList.bitmapcount = ATTR_BIT_MAP_COUNT;
+ AttrList.volattr = ATTR_VOL_CAPABILITIES;
+
+ struct
+ {
+ uint32_t Length;
+ vol_capabilities_attr_t Capabilities;
+ } AttrBuf = {};
+
+ if (getattrlist(Path.c_str(), &AttrList, &AttrBuf, sizeof(AttrBuf), 0) != 0)
+ {
+ return false;
+ }
+
+ // Check that the VOL_CAP_INT_CLONE bit is both valid and set
+ if (!(AttrBuf.Capabilities.valid[VOL_CAPABILITIES_INTERFACES] & VOL_CAP_INT_CLONE))
+ {
+ return false;
+ }
+
+ return !!(AttrBuf.Capabilities.capabilities[VOL_CAPABILITIES_INTERFACES] & VOL_CAP_INT_CLONE);
#else
ZEN_UNUSED(Path);
return false;
@@ -768,7 +857,115 @@ private:
DWORD m_TargetVolumeSerialNumber;
};
-#endif // ZEN_PLATFORM_WINDOWS
+#elif ZEN_PLATFORM_LINUX
+
+class LinuxCloneQueryInterface : public CloneQueryInterface
+{
+public:
+ LinuxCloneQueryInterface(uint64_t AlignmentSize, dev_t TargetDevice) : m_AlignmentSize(AlignmentSize), m_TargetDevice(TargetDevice) {}
+
+ virtual bool CanClone(void* SourceNativeHandle) override
+ {
+ int Fd = int(uintptr_t(SourceNativeHandle));
+
+ struct stat St;
+ if (fstat(Fd, &St) != 0)
+ {
+ return false;
+ }
+
+ // Source must be on the same filesystem as the target
+ return St.st_dev == m_TargetDevice;
+ }
+
+ virtual uint64_t GetClonableRange(uint64_t SourceOffset,
+ uint64_t TargetOffset,
+ uint64_t Size,
+ uint64_t& OutPreBytes,
+ uint64_t& OutPostBytes) override
+ {
+ if (Size < m_AlignmentSize)
+ {
+ return 0;
+ }
+
+ uint64_t PreBytes = (m_AlignmentSize - (SourceOffset % m_AlignmentSize)) % m_AlignmentSize;
+ uint64_t PostBytes = (SourceOffset + Size) % m_AlignmentSize;
+ ZEN_ASSERT(Size >= PreBytes + PostBytes);
+ if (Size - (PreBytes + PostBytes) < m_AlignmentSize)
+ {
+ return 0;
+ }
+ ZEN_ASSERT((PreBytes < Size && PostBytes < Size && Size >= PreBytes + PostBytes + m_AlignmentSize));
+
+ const uint64_t DestCloneOffset = TargetOffset + PreBytes;
+ if (DestCloneOffset % m_AlignmentSize != 0)
+ {
+ return 0;
+ }
+
+ OutPreBytes = PreBytes;
+ OutPostBytes = PostBytes;
+ uint64_t CloneSize = Size - (PreBytes + PostBytes);
+ ZEN_ASSERT(CloneSize % m_AlignmentSize == 0);
+ return CloneSize;
+ }
+
+ virtual bool TryClone(void* SourceNativeHandle,
+ void* TargetNativeHandle,
+ uint64_t AlignedSourceOffset,
+ uint64_t AlignedTargetOffset,
+ uint64_t AlignedSize,
+ uint64_t TargetFinalSize) override
+ {
+ ZEN_ASSERT_SLOW(CanClone(SourceNativeHandle));
+ ZEN_ASSERT((AlignedSourceOffset % m_AlignmentSize) == 0);
+ ZEN_ASSERT((AlignedTargetOffset % m_AlignmentSize) == 0);
+ ZEN_ASSERT(AlignedSize > 0);
+ ZEN_ASSERT((AlignedSize % m_AlignmentSize) == 0);
+
+ int SourceFd = int(uintptr_t(SourceNativeHandle));
+ int TargetFd = int(uintptr_t(TargetNativeHandle));
+
+ // Ensure the target file is sized to its final size before cloning
+ struct stat TargetSt;
+ if (fstat(TargetFd, &TargetSt) != 0 || uint64_t(TargetSt.st_size) != TargetFinalSize)
+ {
+ if (ftruncate(TargetFd, TargetFinalSize) != 0)
+ {
+ std::error_code DummyEc;
+ ZEN_DEBUG("Failed setting final size {} for file {}", TargetFinalSize, PathFromHandle(TargetNativeHandle, DummyEc));
+ return false;
+ }
+ }
+
+ struct file_clone_range Range = {};
+ Range.src_fd = SourceFd;
+ Range.src_offset = AlignedSourceOffset;
+ Range.src_length = AlignedSize;
+ Range.dest_offset = AlignedTargetOffset;
+
+ if (ioctl(TargetFd, FICLONERANGE, &Range) != 0)
+ {
+ std::error_code DummyEc;
+ ZEN_DEBUG("Failed cloning {} bytes from file {} at {} to file {} at {}",
+ AlignedSize,
+ PathFromHandle(SourceNativeHandle, DummyEc),
+ AlignedSourceOffset,
+ PathFromHandle(TargetNativeHandle, DummyEc),
+ AlignedTargetOffset);
+ return false;
+ }
+
+ return true;
+ }
+
+private:
+ uint64_t m_AlignmentSize;
+ dev_t m_TargetDevice;
+};
+
+#endif // ZEN_PLATFORM_WINDOWS / ZEN_PLATFORM_LINUX
std::unique_ptr<CloneQueryInterface>
GetCloneQueryInterface(const std::filesystem::path& TargetDirectory)
@@ -819,7 +1016,30 @@ GetCloneQueryInterface(const std::filesystem::path& TargetDirectory)
return std::make_unique<WindowsCloneQueryInterface>(SectorsPerCluster * BytesPerSector, DestVolumeSerialNumber);
}
}
-#else // ZEN_PLATFORM_WINDOWS
+#elif ZEN_PLATFORM_LINUX
+ struct statfs FsBuf;
+ if (statfs(TargetDirectory.c_str(), &FsBuf) != 0)
+ {
+ ZEN_DEBUG("Failed to get filesystem info for path {}", TargetDirectory);
+ return {};
+ }
+
+ // Only Btrfs and XFS support FICLONERANGE
+ if (FsBuf.f_type != BTRFS_SUPER_MAGIC && FsBuf.f_type != XFS_SUPER_MAGIC)
+ {
+ return {};
+ }
+
+ struct stat StBuf;
+ if (stat(TargetDirectory.c_str(), &StBuf) != 0)
+ {
+ ZEN_DEBUG("Failed to stat path {}", TargetDirectory);
+ return {};
+ }
+
+ uint64_t AlignmentSize = FsBuf.f_bsize;
+ return std::make_unique<LinuxCloneQueryInterface>(AlignmentSize, StBuf.st_dev);
+#else
ZEN_UNUSED(TargetDirectory);
#endif // ZEN_PLATFORM_WINDOWS
return {};
@@ -1000,40 +1220,44 @@ TryCloneFile(const std::filesystem::path& FromPath, const std::filesystem::path&
return TryCloneFile((void*)FromFile.m_Handle, (void*)TargetFile.m_Handle);
#elif ZEN_PLATFORM_LINUX
-# if 0
- struct ScopedFd
- {
- ~ScopedFd() { close(Fd); }
- int Fd;
- };
-
// The 'from' file
- int FromFd = open(FromPath.c_str(), O_RDONLY|O_CLOEXEC);
- if (FromFd < 0)
+ ScopedFd FromFd(open(FromPath.c_str(), O_RDONLY | O_CLOEXEC));
+ if (!FromFd)
{
return false;
}
- ScopedFd $From = { FromFd };
+
+ // Remove any existing target so we can create a fresh clone
+ unlink(ToPath.c_str());
// The 'to' file
- int ToFd = open(ToPath.c_str(), O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, 0666);
- if (ToFd < 0)
+ ScopedFd ToFd(open(ToPath.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0666));
+ if (!ToFd)
{
return false;
}
- fchmod(ToFd, 0666);
- ScopedFd $To = { ToFd };
- ioctl(ToFd, FICLONE, FromFd);
+ 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();
+ unlink(ToPath.c_str());
+ return false;
+ }
- return false;
-# endif // 0
- ZEN_UNUSED(FromPath, ToPath);
- return false;
+ return true;
#elif ZEN_PLATFORM_MAC
- /* clonefile() syscall if APFS */
- ZEN_UNUSED(FromPath, ToPath);
- return false;
+ // Remove any existing target - clonefile() requires the destination not exist
+ unlink(ToPath.c_str());
+
+ 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 true;
#endif // ZEN_PLATFORM_WINDOWS
}
@@ -1069,9 +1293,7 @@ CopyFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToP
if (Options.MustClone)
{
-#if ZEN_PLATFORM_MAC || ZEN_PLATFORM_LINUX
- ZEN_ERROR("CloneFile() is not implemented on this platform");
-#endif // ZEN_PLATFORM_MAC || ZEN_PLATFORM_LINUX
+ ZEN_ERROR("CloneFile() failed for {} -> {}", FromPath, ToPath);
return false;
}
@@ -1084,35 +1306,27 @@ CopyFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToP
&CancelFlag,
/* dwCopyFlags */ 0);
#else
- struct ScopedFd
- {
- ~ScopedFd() { close(Fd); }
- int Fd;
- };
-
// From file
- int FromFd = open(FromPath.c_str(), O_RDONLY | O_CLOEXEC);
- if (FromFd < 0)
+ ScopedFd FromFd(open(FromPath.c_str(), O_RDONLY | O_CLOEXEC));
+ if (!FromFd)
{
ThrowLastError(fmt::format("failed to open file {}", FromPath));
}
- ScopedFd $From = {FromFd};
// To file
- int ToFd = open(ToPath.c_str(), O_WRONLY | O_CREAT | O_CLOEXEC, 0666);
- if (ToFd < 0)
+ ScopedFd ToFd(open(ToPath.c_str(), O_WRONLY | O_CREAT | O_CLOEXEC, 0666));
+ if (!ToFd)
{
ThrowLastError(fmt::format("failed to create file {}", ToPath));
}
- fchmod(ToFd, 0666);
- ScopedFd $To = {ToFd};
+ fchmod(ToFd.Fd, 0666);
struct stat Stat;
- fstat(FromFd, &Stat);
+ fstat(FromFd.Fd, &Stat);
size_t FileSizeBytes = Stat.st_size;
- int $Ignore = fchown(ToFd, Stat.st_uid, Stat.st_gid);
+ int $Ignore = fchown(ToFd.Fd, Stat.st_uid, Stat.st_gid);
ZEN_UNUSED($Ignore); // What's the appropriate error handling here?
// Copy impl
@@ -1120,14 +1334,14 @@ CopyFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToP
void* Buffer = malloc(BufferSize);
while (true)
{
- int BytesRead = read(FromFd, Buffer, BufferSize);
+ int BytesRead = read(FromFd.Fd, Buffer, BufferSize);
if (BytesRead <= 0)
{
Success = (BytesRead == 0);
break;
}
- if (write(ToFd, Buffer, BytesRead) != BytesRead)
+ if (write(ToFd.Fd, Buffer, BytesRead) != BytesRead)
{
Success = false;
break;
@@ -1371,20 +1585,20 @@ WriteFile(std::filesystem::path Path, const IoBuffer* const* Data, size_t Buffer
}
#else
- int OpenFlags = O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC;
- int Fd = open(Path.c_str(), OpenFlags, 0666);
- if (Fd < 0)
+ int OpenFlags = O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC;
+ ScopedFd OutFd(open(Path.c_str(), OpenFlags, 0666));
+ if (!OutFd)
{
zen::CreateDirectories(Path.parent_path());
- Fd = open(Path.c_str(), OpenFlags, 0666);
+ OutFd = ScopedFd(open(Path.c_str(), OpenFlags, 0666));
}
- if (Fd < 0)
+ if (!OutFd)
{
ThrowLastError(fmt::format("File open failed for '{}'", Path));
}
- fchmod(Fd, 0666);
+ fchmod(OutFd.Fd, 0666);
#endif
// TODO: this should be block-enlightened
@@ -1408,9 +1622,9 @@ WriteFile(std::filesystem::path Path, const IoBuffer* const* Data, size_t Buffer
ThrowSystemException(hRes, fmt::format("File write failed for '{}'", Path).c_str());
}
#else
- if (write(Fd, DataPtr, ChunkSize) != int64_t(ChunkSize))
+ if (write(OutFd.Fd, DataPtr, ChunkSize) != int64_t(ChunkSize))
{
- close(Fd);
+ OutFd = ScopedFd();
std::error_code DummyEc;
RemoveFile(Path, DummyEc);
ThrowLastError(fmt::format("File write failed for '{}'", Path));
@@ -1424,8 +1638,6 @@ WriteFile(std::filesystem::path Path, const IoBuffer* const* Data, size_t Buffer
#if ZEN_PLATFORM_WINDOWS
Outfile.Close();
-#else
- close(Fd);
#endif
}
@@ -1707,8 +1919,8 @@ ScanFile(std::filesystem::path Path, const uint64_t ChunkSize, std::function<voi
ProcessFunc(ReadBuffer.data(), dwBytesRead);
}
#else
- int Fd = open(Path.c_str(), O_RDONLY | O_CLOEXEC);
- if (Fd < 0)
+ ScopedFd InFd(open(Path.c_str(), O_RDONLY | O_CLOEXEC));
+ if (!InFd)
{
return false;
}
@@ -1718,7 +1930,7 @@ ScanFile(std::filesystem::path Path, const uint64_t ChunkSize, std::function<voi
void* Buffer = malloc(ChunkSize);
while (true)
{
- int BytesRead = read(Fd, Buffer, ChunkSize);
+ int BytesRead = read(InFd.Fd, Buffer, ChunkSize);
if (BytesRead < 0)
{
Success = false;
@@ -1734,7 +1946,6 @@ ScanFile(std::filesystem::path Path, const uint64_t ChunkSize, std::function<voi
}
free(Buffer);
- close(Fd);
if (!Success)
{
@@ -3123,28 +3334,26 @@ public:
ZEN_UNUSED(SystemGlobal);
std::string InstanceMapName = fmt::format("/{}", Name);
- int Fd = shm_open(InstanceMapName.c_str(), O_RDWR, 0666);
- if (Fd < 0)
+ ScopedFd FdGuard(shm_open(InstanceMapName.c_str(), O_RDWR, 0666));
+ if (!FdGuard)
{
return {};
}
- void* hMap = (void*)intptr_t(Fd);
struct stat Stat;
- fstat(Fd, &Stat);
+ fstat(FdGuard.Fd, &Stat);
if (size_t(Stat.st_size) < Size)
{
- close(Fd);
return {};
}
- void* pBuf = mmap(nullptr, Size, PROT_READ | PROT_WRITE, MAP_SHARED, Fd, 0);
+ void* pBuf = mmap(nullptr, Size, PROT_READ | PROT_WRITE, MAP_SHARED, FdGuard.Fd, 0);
if (pBuf == MAP_FAILED)
{
- close(Fd);
return {};
}
+ void* hMap = (void*)intptr_t(FdGuard.Release());
return Data{.Handle = hMap, .DataPtr = pBuf, .Size = Size, .Name = std::string(Name)};
#endif // ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
}
@@ -3199,23 +3408,22 @@ public:
ZEN_UNUSED(SystemGlobal);
std::string InstanceMapName = fmt::format("/{}", Name);
- int Fd = shm_open(InstanceMapName.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0666);
- if (Fd < 0)
+ ScopedFd FdGuard(shm_open(InstanceMapName.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0666));
+ if (!FdGuard)
{
return {};
}
- fchmod(Fd, 0666);
- void* hMap = (void*)intptr_t(Fd);
+ fchmod(FdGuard.Fd, 0666);
- int Result = ftruncate(Fd, Size);
+ int Result = ftruncate(FdGuard.Fd, Size);
ZEN_UNUSED(Result);
- void* pBuf = mmap(nullptr, Size, PROT_READ | PROT_WRITE, MAP_SHARED, Fd, 0);
+ void* pBuf = mmap(nullptr, Size, PROT_READ | PROT_WRITE, MAP_SHARED, FdGuard.Fd, 0);
if (pBuf == MAP_FAILED)
{
- close(Fd);
return {};
}
+ void* hMap = (void*)intptr_t(FdGuard.Release());
return Data{.Handle = hMap, .DataPtr = pBuf, .Size = Size, .Name = std::string(Name)};
#endif // ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
}
@@ -3590,6 +3798,241 @@ TEST_CASE("RotateDirectories")
}
}
+TEST_CASE("TryCloneFile")
+{
+ std::filesystem::path TestBaseDir = GetRunningExecutablePath().parent_path() / ".clone_test";
+ CleanDirectory(TestBaseDir, true);
+
+ SUBCASE("clone produces identical content")
+ {
+ std::filesystem::path SrcPath = TestBaseDir / "src.bin";
+ std::filesystem::path DstPath = TestBaseDir / "dst.bin";
+
+ // Write source file with known content
+ const char Content[] = "Hello, clone world! This is test data for TryCloneFile.";
+ WriteFile(SrcPath, IoBuffer(IoBuffer::Wrap, Content, sizeof(Content)));
+ CHECK(IsFile(SrcPath));
+
+ bool Cloned = TryCloneFile(SrcPath, DstPath);
+
+ if (Cloned)
+ {
+ CHECK(IsFile(DstPath));
+ CHECK_EQ(FileSizeFromPath(DstPath), sizeof(Content));
+
+ FileContents DstContents = ReadFile(DstPath);
+ CHECK(DstContents);
+ CHECK_EQ(DstContents.Data[0].GetSize(), sizeof(Content));
+ CHECK_EQ(memcmp(DstContents.Data[0].Data(), Content, sizeof(Content)), 0);
+ }
+ 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");
+ }
+ }
+
+ SUBCASE("clone overwrites existing target")
+ {
+ std::filesystem::path SrcPath = TestBaseDir / "src_overwrite.bin";
+ std::filesystem::path DstPath = TestBaseDir / "dst_overwrite.bin";
+
+ const char OldContent[] = "old content";
+ const char NewContent[] = "new content that is longer than the old one";
+ WriteFile(DstPath, IoBuffer(IoBuffer::Wrap, OldContent, sizeof(OldContent)));
+ WriteFile(SrcPath, IoBuffer(IoBuffer::Wrap, NewContent, sizeof(NewContent)));
+
+ bool Cloned = TryCloneFile(SrcPath, DstPath);
+
+ if (Cloned)
+ {
+ CHECK_EQ(FileSizeFromPath(DstPath), sizeof(NewContent));
+
+ FileContents DstContents = ReadFile(DstPath);
+ CHECK(DstContents);
+ CHECK_EQ(memcmp(DstContents.Data[0].Data(), NewContent, sizeof(NewContent)), 0);
+ }
+ }
+
+ SUBCASE("clone of nonexistent source fails")
+ {
+ std::filesystem::path SrcPath = TestBaseDir / "no_such_file.bin";
+ std::filesystem::path DstPath = TestBaseDir / "dst_nosrc.bin";
+
+ CHECK_FALSE(TryCloneFile(SrcPath, DstPath));
+ CHECK_FALSE(IsFile(DstPath));
+ }
+
+ DeleteDirectories(TestBaseDir);
+}
+
+TEST_CASE("CopyFile.Clone")
+{
+ std::filesystem::path TestBaseDir = GetRunningExecutablePath().parent_path() / ".copyfile_clone_test";
+ CleanDirectory(TestBaseDir, true);
+
+ const char Content[] = "CopyFile clone test content with some bytes to verify integrity.";
+ std::filesystem::path SrcPath = TestBaseDir / "src.bin";
+ WriteFile(SrcPath, IoBuffer(IoBuffer::Wrap, Content, sizeof(Content)));
+
+ SUBCASE("EnableClone copies file regardless of clone support")
+ {
+ std::filesystem::path DstPath = TestBaseDir / "dst_enable.bin";
+
+ CopyFileOptions Options;
+ Options.EnableClone = true;
+ bool Success = CopyFile(SrcPath, DstPath, Options);
+ CHECK(Success);
+ CHECK(IsFile(DstPath));
+ CHECK_EQ(FileSizeFromPath(DstPath), sizeof(Content));
+
+ FileContents DstContents = ReadFile(DstPath);
+ CHECK(DstContents);
+ CHECK_EQ(memcmp(DstContents.Data[0].Data(), Content, sizeof(Content)), 0);
+ }
+
+ SUBCASE("DisableClone still copies file")
+ {
+ std::filesystem::path DstPath = TestBaseDir / "dst_disable.bin";
+
+ CopyFileOptions Options;
+ Options.EnableClone = false;
+ bool Success = CopyFile(SrcPath, DstPath, Options);
+ CHECK(Success);
+ CHECK(IsFile(DstPath));
+ CHECK_EQ(FileSizeFromPath(DstPath), sizeof(Content));
+
+ FileContents DstContents = ReadFile(DstPath);
+ CHECK(DstContents);
+ CHECK_EQ(memcmp(DstContents.Data[0].Data(), Content, sizeof(Content)), 0);
+ }
+
+ DeleteDirectories(TestBaseDir);
+}
+
+TEST_CASE("SupportsBlockRefCounting")
+{
+ std::filesystem::path BinDir = GetRunningExecutablePath().parent_path();
+
+ // Should not crash or throw on a valid path
+ bool Supported = SupportsBlockRefCounting(BinDir);
+ ZEN_INFO("SupportsBlockRefCounting({}) = {}", BinDir, Supported);
+
+ // Should return false for nonexistent path
+ CHECK_FALSE(SupportsBlockRefCounting("/no/such/path/anywhere"));
+}
+
+TEST_CASE("CloneQueryInterface")
+{
+ std::filesystem::path TestBaseDir = GetRunningExecutablePath().parent_path() / ".clonequery_test";
+ CleanDirectory(TestBaseDir, true);
+
+ auto CloneQuery = GetCloneQueryInterface(TestBaseDir);
+
+ if (CloneQuery)
+ {
+ ZEN_INFO("CloneQueryInterface available for {}", TestBaseDir);
+
+ // Write a source file large enough to exercise alignment
+ const uint64_t FileSize = 256 * 1024;
+ IoBuffer SrcBuf(FileSize);
+ {
+ uint8_t* Ptr = SrcBuf.MutableData<uint8_t>();
+ for (uint64_t i = 0; i < FileSize; i++)
+ {
+ Ptr[i] = uint8_t(i * 37 + 7);
+ }
+ }
+
+ std::filesystem::path SrcPath = TestBaseDir / "clone_src.bin";
+ std::filesystem::path DstPath = TestBaseDir / "clone_dst.bin";
+ WriteFile(SrcPath, SrcBuf);
+
+ // Open source and target as native handles
+# if ZEN_PLATFORM_WINDOWS
+ windows::Handle SrcHandle(CreateFileW(SrcPath.c_str(),
+ GENERIC_READ,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ nullptr,
+ OPEN_EXISTING,
+ 0,
+ nullptr));
+ CHECK(SrcHandle != INVALID_HANDLE_VALUE);
+ void* SrcNativeHandle = (void*)SrcHandle.m_Handle;
+
+ windows::Handle DstHandle(
+ CreateFileW(DstPath.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, nullptr, OPEN_ALWAYS, 0, nullptr));
+ CHECK(DstHandle != INVALID_HANDLE_VALUE);
+ void* DstNativeHandle = (void*)DstHandle.m_Handle;
+# else
+ ScopedFd SrcFd(open(SrcPath.c_str(), O_RDONLY | O_CLOEXEC));
+ CHECK(bool(SrcFd));
+ void* SrcNativeHandle = (void*)uintptr_t(SrcFd.Fd);
+
+ ScopedFd DstFd(open(DstPath.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0666));
+ CHECK(bool(DstFd));
+ void* DstNativeHandle = (void*)uintptr_t(DstFd.Fd);
+# endif
+
+ SUBCASE("CanClone returns true for same volume") { CHECK(CloneQuery->CanClone(SrcNativeHandle)); }
+
+ SUBCASE("GetClonableRange and TryClone")
+ {
+ uint64_t PreBytes = 0;
+ uint64_t PostBytes = 0;
+ uint64_t Clonable = CloneQuery->GetClonableRange(0, 0, FileSize, PreBytes, PostBytes);
+
+ if (Clonable > 0)
+ {
+ CHECK_EQ(PreBytes, 0); // Offset 0 is always aligned
+ CHECK(Clonable + PostBytes == FileSize);
+
+ bool Cloned = CloneQuery->TryClone(SrcNativeHandle, DstNativeHandle, 0, 0, Clonable, FileSize);
+ CHECK(Cloned);
+
+ if (Cloned)
+ {
+ // Write the post-alignment tail if any
+ if (PostBytes > 0)
+ {
+ const uint8_t* SrcData = SrcBuf.Data<uint8_t>() + Clonable;
+# if ZEN_PLATFORM_WINDOWS
+ DWORD Written = 0;
+ OVERLAPPED Ov = {};
+ Ov.Offset = (DWORD)(Clonable & 0xFFFFFFFF);
+ Ov.OffsetHigh = (DWORD)(Clonable >> 32);
+ ::WriteFile(DstHandle, SrcData, (DWORD)PostBytes, &Written, &Ov);
+# else
+ pwrite(DstFd.Fd, SrcData, PostBytes, Clonable);
+# endif
+ }
+
+ // Close handles before reading back the file for verification
+# if ZEN_PLATFORM_WINDOWS
+ SrcHandle.Close();
+ DstHandle.Close();
+# else
+ SrcFd = ScopedFd();
+ DstFd = ScopedFd();
+# endif
+
+ FileContents DstContents = ReadFile(DstPath);
+ CHECK(DstContents);
+ IoBuffer DstFlat = DstContents.Flatten();
+ CHECK_EQ(DstFlat.GetSize(), FileSize);
+ CHECK_EQ(memcmp(DstFlat.Data(), SrcBuf.Data(), FileSize), 0);
+ }
+ }
+ }
+ }
+ else
+ {
+ ZEN_INFO("CloneQueryInterface not available for {} (filesystem does not support block cloning)", TestBaseDir);
+ }
+
+ DeleteDirectories(TestBaseDir);
+}
+
TEST_CASE("SharedMemory")
{
CHECK(!OpenSharedMemory("SharedMemoryTest0", 482, false));
diff --git a/src/zencore/include/zencore/filesystem.h b/src/zencore/include/zencore/filesystem.h
index 16e2b59f8..6dc159a83 100644
--- a/src/zencore/include/zencore/filesystem.h
+++ b/src/zencore/include/zencore/filesystem.h
@@ -187,6 +187,14 @@ void ScanFile(void* NativeHandle,
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);
+// Interface for sub-file range cloning on filesystems that support copy-on-write.
+// GetCloneQueryInterface() returns nullptr on platforms without range clone support.
+//
+// Platform capabilities:
+// Windows (ReFS) - True CoW range cloning via FSCTL_DUPLICATE_EXTENTS_TO_FILE.
+// Linux (Btrfs/XFS) - True CoW range cloning via FICLONERANGE ioctl.
+// macOS (APFS) - Not implemented. No sub-file range clone API exists.
+// Whole-file CoW cloning is available via TryCloneFile (clonefile syscall).
class CloneQueryInterface
{
public:
diff --git a/src/zencore/include/zencore/logbase.h b/src/zencore/include/zencore/logbase.h
index ad2ab218d..046e96db3 100644
--- a/src/zencore/include/zencore/logbase.h
+++ b/src/zencore/include/zencore/logbase.h
@@ -101,7 +101,7 @@ struct LoggerRef
inline logging::Logger* operator->() const;
inline logging::Logger& operator*() const;
- bool ShouldLog(logging::LogLevel Level) const { return m_Logger->ShouldLog(Level); }
+ bool ShouldLog(logging::LogLevel Level) const { return m_Logger && m_Logger->ShouldLog(Level); }
void SetLogLevel(logging::LogLevel NewLogLevel) { m_Logger->SetLevel(NewLogLevel); }
logging::LogLevel GetLogLevel() { return m_Logger->GetLevel(); }
diff --git a/src/zencore/include/zencore/process.h b/src/zencore/include/zencore/process.h
index 809312c7b..3177f64c1 100644
--- a/src/zencore/include/zencore/process.h
+++ b/src/zencore/include/zencore/process.h
@@ -6,6 +6,9 @@
#include <zencore/zencore.h>
#include <filesystem>
+#include <string>
+#include <utility>
+#include <vector>
namespace zen {
@@ -68,6 +71,12 @@ struct CreateProcOptions
const std::filesystem::path* WorkingDirectory = nullptr;
uint32_t Flags = 0;
std::filesystem::path StdoutFile;
+
+ /// Additional environment variables for the child process. These are merged
+ /// 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
#endif
diff --git a/src/zencore/include/zencore/string.h b/src/zencore/include/zencore/string.h
index 4deca63ed..60293a313 100644
--- a/src/zencore/include/zencore/string.h
+++ b/src/zencore/include/zencore/string.h
@@ -331,9 +331,10 @@ public:
return AppendAscii(Str);
}
-#if defined(__clang__) && !defined(__APPLE__)
+#if defined(__clang__) && !defined(__APPLE__) && !defined(_MSC_VER)
/* UE Toolchain Clang has different types for int64_t and long long so an override is
- needed here. Without it, Clang can't disambiguate integer overloads */
+ needed here. Without it, Clang can't disambiguate integer overloads.
+ On MSVC ABI (including clang-cl), int64_t is long long so no separate overload is needed. */
inline StringBuilderImpl& operator<<(long long n)
{
IntNum Str(n);
@@ -953,6 +954,24 @@ StrCaseCompare(const char* Lhs, const char* Rhs, int64_t Length = -1)
#endif
}
+inline int32_t
+StrCaseCompare(std::string_view Lhs, std::string_view Rhs)
+{
+ int32_t Result = StrCaseCompare(Lhs.data(), Rhs.data(), std::min(Lhs.size(), Rhs.size()));
+ if (Result == 0)
+ {
+ if (Lhs.size() < Rhs.size())
+ {
+ return -1;
+ }
+ else if (Lhs.size() > Rhs.size())
+ {
+ return 1;
+ }
+ }
+ return Result;
+}
+
/**
* @brief
* Helper function to implement case sensitive spaceship operator for strings.
diff --git a/src/zencore/include/zencore/system.h b/src/zencore/include/zencore/system.h
index a67999e52..2e39cc660 100644
--- a/src/zencore/include/zencore/system.h
+++ b/src/zencore/include/zencore/system.h
@@ -17,6 +17,7 @@ std::string_view GetOperatingSystemName();
std::string GetOperatingSystemVersion();
std::string_view GetRuntimePlatformName(); // "windows", "wine", "linux", or "macos"
std::string_view GetCpuName();
+std::string_view GetCompilerName();
struct SystemMetrics
{
diff --git a/src/zencore/memtrack/callstacktrace.cpp b/src/zencore/memtrack/callstacktrace.cpp
index 4a7068568..ccbea1282 100644
--- a/src/zencore/memtrack/callstacktrace.cpp
+++ b/src/zencore/memtrack/callstacktrace.cpp
@@ -193,8 +193,12 @@ private:
std::atomic_uint32_t CallstackIdCounter{1}; // 0 is reserved for "unknown callstack"
};
+} // namespace zen
+
# if UE_CALLSTACK_TRACE_USE_UNWIND_TABLES
+namespace zen {
+
/*
* Windows' x64 binaries contain a ".pdata" section that describes the location
* and size of its functions and details on how to unwind them. The unwind
@@ -908,98 +912,110 @@ FBacktracer::GetBacktraceId(void* AddressOfReturnAddress)
// queue (i.e. the processing thread has caught up processing).
return CallstackTracer.AddCallstack(BacktraceEntry);
}
-}
+} // namespace zen
# else // UE_CALLSTACK_TRACE_USE_UNWIND_TABLES
namespace zen {
- ////////////////////////////////////////////////////////////////////////////////
- class FBacktracer
- {
- public:
- FBacktracer(FMalloc* InMalloc);
- ~FBacktracer();
- static FBacktracer* Get();
- inline uint32_t GetBacktraceId(void* AddressOfReturnAddress);
- uint32_t GetBacktraceId(uint64_t ReturnAddress);
- void AddModule(uintptr_t Base, const char16_t* Name) {}
- void RemoveModule(uintptr_t Base) {}
-
- private:
- static FBacktracer* Instance;
- FMalloc* Malloc;
- FCallstackTracer CallstackTracer;
- };
+////////////////////////////////////////////////////////////////////////////////
+class FBacktracer
+{
+public:
+ FBacktracer(FMalloc* InMalloc);
+ ~FBacktracer();
+ static FBacktracer* Get();
+ inline uint32_t GetBacktraceId(void* AddressOfReturnAddress);
+ uint32_t GetBacktraceId(uint64_t ReturnAddress);
+ void AddModule(uintptr_t /*Base*/, const char16_t* /*Name*/) {}
+ void RemoveModule(uintptr_t /*Base*/) {}
- ////////////////////////////////////////////////////////////////////////////////
- FBacktracer* FBacktracer::Instance = nullptr;
+private:
+ static FBacktracer* Instance;
+ FMalloc* Malloc;
+ FCallstackTracer CallstackTracer;
+};
- ////////////////////////////////////////////////////////////////////////////////
- FBacktracer::FBacktracer(FMalloc* InMalloc) : Malloc(InMalloc), CallstackTracer(InMalloc) { Instance = this; }
+////////////////////////////////////////////////////////////////////////////////
+FBacktracer* FBacktracer::Instance = nullptr;
- ////////////////////////////////////////////////////////////////////////////////
- FBacktracer::~FBacktracer() {}
+////////////////////////////////////////////////////////////////////////////////
+FBacktracer::FBacktracer(FMalloc* InMalloc) : Malloc(InMalloc), CallstackTracer(InMalloc)
+{
+ Instance = this;
+}
- ////////////////////////////////////////////////////////////////////////////////
- FBacktracer* FBacktracer::Get() { return Instance; }
+////////////////////////////////////////////////////////////////////////////////
+FBacktracer::~FBacktracer()
+{
+}
- ////////////////////////////////////////////////////////////////////////////////
- uint32_t FBacktracer::GetBacktraceId(void* AddressOfReturnAddress)
- {
- const uint64_t ReturnAddress = *(uint64_t*)AddressOfReturnAddress;
- return GetBacktraceId(ReturnAddress);
- }
+////////////////////////////////////////////////////////////////////////////////
+FBacktracer*
+FBacktracer::Get()
+{
+ return Instance;
+}
- ////////////////////////////////////////////////////////////////////////////////
- uint32_t FBacktracer::GetBacktraceId(uint64_t ReturnAddress)
- {
+////////////////////////////////////////////////////////////////////////////////
+uint32_t
+FBacktracer::GetBacktraceId(void* AddressOfReturnAddress)
+{
+ const uint64_t ReturnAddress = *(uint64_t*)AddressOfReturnAddress;
+ return GetBacktraceId(ReturnAddress);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+uint32_t
+FBacktracer::GetBacktraceId(uint64_t ReturnAddress)
+{
+ ZEN_UNUSED(ReturnAddress);
# if !UE_BUILD_SHIPPING
- uint64_t StackFrames[256];
- int32_t NumStackFrames = FPlatformStackWalk::CaptureStackBackTrace(StackFrames, UE_ARRAY_COUNT(StackFrames));
- if (NumStackFrames > 0)
+ uint64_t StackFrames[256];
+ int32_t NumStackFrames = FPlatformStackWalk::CaptureStackBackTrace(StackFrames, UE_ARRAY_COUNT(StackFrames));
+ if (NumStackFrames > 0)
+ {
+ FCallstackTracer::FBacktraceEntry BacktraceEntry;
+ uint64_t BacktraceId = 0;
+ uint32_t FrameIdx = 0;
+ bool bUseAddress = false;
+ for (int32_t Index = 0; Index < NumStackFrames; Index++)
{
- FCallstackTracer::FBacktraceEntry BacktraceEntry;
- uint64_t BacktraceId = 0;
- uint32_t FrameIdx = 0;
- bool bUseAddress = false;
- for (int32_t Index = 0; Index < NumStackFrames; Index++)
+ if (!bUseAddress)
{
- if (!bUseAddress)
- {
- // start using backtrace only after ReturnAddress
- if (StackFrames[Index] == (uint64_t)ReturnAddress)
- {
- bUseAddress = true;
- }
- }
- if (bUseAddress || NumStackFrames == 1)
+ // start using backtrace only after ReturnAddress
+ if (StackFrames[Index] == (uint64_t)ReturnAddress)
{
- uint64_t RetAddr = StackFrames[Index];
- StackFrames[FrameIdx++] = RetAddr;
-
- // This is a simple order-dependent LCG. Should be sufficient enough
- BacktraceId += RetAddr;
- BacktraceId *= 0x30be8efa499c249dull;
+ bUseAddress = true;
}
}
+ if (bUseAddress || NumStackFrames == 1)
+ {
+ uint64_t RetAddr = StackFrames[Index];
+ StackFrames[FrameIdx++] = RetAddr;
- // Save the collected id
- BacktraceEntry.Hash = BacktraceId;
- BacktraceEntry.FrameCount = FrameIdx;
- BacktraceEntry.Frames = StackFrames;
-
- // Add to queue to be processed. This might block until there is room in the
- // queue (i.e. the processing thread has caught up processing).
- return CallstackTracer.AddCallstack(BacktraceEntry);
+ // This is a simple order-dependent LCG. Should be sufficient enough
+ BacktraceId += RetAddr;
+ BacktraceId *= 0x30be8efa499c249dull;
+ }
}
-# endif
- return 0;
+ // Save the collected id
+ BacktraceEntry.Hash = BacktraceId;
+ BacktraceEntry.FrameCount = FrameIdx;
+ BacktraceEntry.Frames = StackFrames;
+
+ // Add to queue to be processed. This might block until there is room in the
+ // queue (i.e. the processing thread has caught up processing).
+ return CallstackTracer.AddCallstack(BacktraceEntry);
}
+# endif
+ return 0;
}
+} // namespace zen
+
# endif // UE_CALLSTACK_TRACE_USE_UNWIND_TABLES
namespace zen {
@@ -1047,7 +1063,7 @@ CallstackTrace_GetCurrentId()
# if PLATFORM_USE_CALLSTACK_ADDRESS_POINTER
return Instance->GetBacktraceId(StackAddress);
# else
- return Instance->GetBacktraceId((uint64_t)StackAddress);
+ return Instance->GetBacktraceId((uint64_t)StackAddress);
# endif
}
diff --git a/src/zencore/process.cpp b/src/zencore/process.cpp
index f657869dc..080607f13 100644
--- a/src/zencore/process.cpp
+++ b/src/zencore/process.cpp
@@ -11,6 +11,7 @@
#include <zencore/timer.h>
#include <zencore/trace.h>
+#include <map>
#include <thread>
ZEN_THIRD_PARTY_INCLUDES_START
@@ -20,8 +21,8 @@ ZEN_THIRD_PARTY_INCLUDES_START
# include <Psapi.h>
# include <shellapi.h>
-# include <Shlobj.h>
-# include <TlHelp32.h>
+# include <shlobj.h>
+# include <tlhelp32.h>
#else
# include <fcntl.h>
# include <pthread.h>
@@ -487,13 +488,57 @@ CreateProcNormal(const std::filesystem::path& Executable, std::string_view Comma
STARTUPINFO StartupInfo{.cb = sizeof(STARTUPINFO)};
bool InheritHandles = false;
- void* Environment = nullptr;
LPSECURITY_ATTRIBUTES ProcessAttributes = nullptr;
LPSECURITY_ATTRIBUTES ThreadAttributes = nullptr;
+ // Build environment block when custom environment variables are specified
+ ExtendableWideStringBuilder<512> EnvironmentBlock;
+ void* Environment = nullptr;
+ if (!Options.Environment.empty())
+ {
+ // Capture current environment into a map
+ std::map<std::wstring, std::wstring> EnvMap;
+ wchar_t* EnvStrings = GetEnvironmentStringsW();
+ if (EnvStrings)
+ {
+ for (const wchar_t* Ptr = EnvStrings; *Ptr; Ptr += wcslen(Ptr) + 1)
+ {
+ std::wstring_view Entry(Ptr);
+ size_t EqPos = Entry.find(L'=');
+ if (EqPos != std::wstring_view::npos && EqPos > 0)
+ {
+ EnvMap[std::wstring(Entry.substr(0, EqPos))] = std::wstring(Entry.substr(EqPos + 1));
+ }
+ }
+ FreeEnvironmentStringsW(EnvStrings);
+ }
+
+ // Apply overrides
+ for (const auto& [Key, Value] : Options.Environment)
+ {
+ EnvMap[Utf8ToWide(Key)] = Utf8ToWide(Value);
+ }
+
+ // Build double-null-terminated environment block
+ for (const auto& [Key, Value] : EnvMap)
+ {
+ EnvironmentBlock << Key;
+ EnvironmentBlock.Append(L'=');
+ EnvironmentBlock << Value;
+ EnvironmentBlock.Append(L'\0');
+ }
+ EnvironmentBlock.Append(L'\0');
+
+ Environment = EnvironmentBlock.Data();
+ }
+
const bool AssignToJob = Options.AssignToJob && Options.AssignToJob->IsValid();
DWORD CreationFlags = 0;
+ if (Environment)
+ {
+ CreationFlags |= CREATE_UNICODE_ENVIRONMENT;
+ }
if (Options.Flags & CreateProcOptions::Flag_NewConsole)
{
CreationFlags |= CREATE_NEW_CONSOLE;
@@ -790,6 +835,11 @@ CreateProc(const std::filesystem::path& Executable, std::string_view CommandLine
}
}
+ for (const auto& [Key, Value] : Options.Environment)
+ {
+ setenv(Key.c_str(), Value.c_str(), 1);
+ }
+
if (execv(Executable.c_str(), ArgV.data()) < 0)
{
ThrowLastError("Failed to exec() a new process image");
diff --git a/src/zencore/sentryintegration.cpp b/src/zencore/sentryintegration.cpp
index 58b76783a..b7d01003b 100644
--- a/src/zencore/sentryintegration.cpp
+++ b/src/zencore/sentryintegration.cpp
@@ -31,6 +31,8 @@ namespace {
struct SentryAssertImpl : zen::AssertImpl
{
+ ZEN_DEBUG_SECTION ~SentryAssertImpl() override = default;
+
virtual void ZEN_FORCENOINLINE ZEN_DEBUG_SECTION OnAssert(const char* Filename,
int LineNumber,
const char* FunctionName,
diff --git a/src/zencore/string.cpp b/src/zencore/string.cpp
index ed0ba6f46..358722b0b 100644
--- a/src/zencore/string.cpp
+++ b/src/zencore/string.cpp
@@ -1181,6 +1181,17 @@ TEST_CASE("string")
CHECK(StrCaseCompare("BBr", "Bar", 2) > 0);
}
+ SUBCASE("StrCaseCompare")
+ {
+ CHECK(StrCaseCompare("foo"sv, "FoO"sv) == 0);
+ CHECK(StrCaseCompare("foo"sv, "FoOz"sv) < 0);
+ CHECK(StrCaseCompare("fooo"sv, "FoO"sv) > 0);
+ CHECK(StrCaseCompare("Bar"sv, "bAs"sv) < 0);
+ CHECK(StrCaseCompare("bAr"sv, "Bas"sv) < 0);
+ CHECK(StrCaseCompare("BBr"sv, "Bar"sv) > 0);
+ CHECK(StrCaseCompare("Bbr"sv, "BAr"sv) > 0);
+ }
+
SUBCASE("ForEachStrTok")
{
const auto Tokens = "here,is,my,different,tokens"sv;
diff --git a/src/zencore/system.cpp b/src/zencore/system.cpp
index 141450b84..8985a8a76 100644
--- a/src/zencore/system.cpp
+++ b/src/zencore/system.cpp
@@ -660,6 +660,24 @@ GetCpuName()
#endif
}
+std::string_view
+GetCompilerName()
+{
+#define ZEN_STRINGIFY_IMPL(x) #x
+#define ZEN_STRINGIFY(x) ZEN_STRINGIFY_IMPL(x)
+#if ZEN_COMPILER_CLANG
+ return "clang " ZEN_STRINGIFY(__clang_major__) "." ZEN_STRINGIFY(__clang_minor__) "." ZEN_STRINGIFY(__clang_patchlevel__);
+#elif ZEN_COMPILER_MSC
+ return "MSVC " ZEN_STRINGIFY(_MSC_VER);
+#elif ZEN_COMPILER_GCC
+ return "GCC " ZEN_STRINGIFY(__GNUC__) "." ZEN_STRINGIFY(__GNUC_MINOR__) "." ZEN_STRINGIFY(__GNUC_PATCHLEVEL__);
+#else
+ return "unknown";
+#endif
+#undef ZEN_STRINGIFY
+#undef ZEN_STRINGIFY_IMPL
+}
+
void
Describe(const SystemMetrics& Metrics, CbWriter& Writer)
{
diff --git a/src/zencore/testing.cpp b/src/zencore/testing.cpp
index d7eb3b17d..f5bc723b1 100644
--- a/src/zencore/testing.cpp
+++ b/src/zencore/testing.cpp
@@ -24,6 +24,8 @@
# if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
# include <execinfo.h>
# include <unistd.h>
+# elif ZEN_PLATFORM_WINDOWS
+# include <crtdbg.h>
# endif
namespace zen::testing {
@@ -296,6 +298,17 @@ RunTestMain(int Argc, char* Argv[], const char* ExecutableName, void (*ForceLink
}
# endif
+# if ZEN_PLATFORM_WINDOWS
+ // Suppress Windows error dialogs (crash/abort/assert) so tests terminate
+ // immediately instead of blocking on a modal dialog in CI or headless runs.
+ SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
+ _set_abort_behavior(0, _WRITE_ABORT_MSG);
+ _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE);
+ _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);
+ _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE);
+ _CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR);
+# endif
+
zen::logging::InitializeLogging();
zen::MaximizeOpenFileCount();
InstallCrashSignalHandlers();