diff options
| author | Stefan Boberg <[email protected]> | 2023-12-11 13:09:03 +0100 |
|---|---|---|
| committer | Stefan Boberg <[email protected]> | 2023-12-11 13:09:03 +0100 |
| commit | 93afeddbc7a5b5df390a29407f5515acd5a70fc1 (patch) | |
| tree | 6f85ee551aabe20dece64a750c0b2d5d2c5d2d5d /src/zencore | |
| parent | removed unnecessary SHA1 references (diff) | |
| parent | Make sure that PathFromHandle don't hide true error when throwing exceptions ... (diff) | |
| download | zen-93afeddbc7a5b5df390a29407f5515acd5a70fc1.tar.xz zen-93afeddbc7a5b5df390a29407f5515acd5a70fc1.zip | |
Merge branch 'main' of https://github.com/EpicGames/zen
Diffstat (limited to 'src/zencore')
22 files changed, 1574 insertions, 866 deletions
diff --git a/src/zencore/compactbinary.cpp b/src/zencore/compactbinary.cpp index 5e8ce22ed..9152a8bfc 100644 --- a/src/zencore/compactbinary.cpp +++ b/src/zencore/compactbinary.cpp @@ -2,6 +2,7 @@ #include "zencore/compactbinary.h" +#include <zencore/assertfmt.h> #include <zencore/base64.h> #include <zencore/compactbinarybuilder.h> #include <zencore/compactbinaryvalidation.h> @@ -855,10 +856,10 @@ void CbFieldView::CopyTo(MutableMemoryView Buffer) const { const MemoryView Source = GetViewNoType(); - ZEN_ASSERT(Buffer.GetSize() == sizeof(CbFieldType) + Source.GetSize()); - // TEXT("A buffer of %" UINT64_FMT " bytes was provided when %" UINT64_FMT " bytes are required"), - // Buffer.GetSize(), - // sizeof(CbFieldType) + Source.GetSize()); + ZEN_ASSERT_FORMAT(Buffer.GetSize() == sizeof(CbFieldType) + Source.GetSize(), + "A buffer of {} bytes was provided when {} bytes are required", + Buffer.GetSize(), + sizeof(CbFieldType) + Source.GetSize()); *static_cast<CbFieldType*>(Buffer.GetData()) = CbFieldTypeOps::GetSerializedType(Type); Buffer.RightChopInline(sizeof(CbFieldType)); memcpy(Buffer.GetData(), Source.GetData(), Source.GetSize()); @@ -963,10 +964,10 @@ void CbArrayView::CopyTo(MutableMemoryView Buffer) const { const MemoryView Source = GetPayloadView(); - ZEN_ASSERT(Buffer.GetSize() == sizeof(CbFieldType) + Source.GetSize()); - // TEXT("Buffer is %" UINT64_FMT " bytes but %" UINT64_FMT " is required."), - // Buffer.GetSize(), - // sizeof(CbFieldType) + Source.GetSize()); + ZEN_ASSERT_FORMAT(Buffer.GetSize() == sizeof(CbFieldType) + Source.GetSize(), + "Buffer is {} bytes but {} is required.", + Buffer.GetSize(), + sizeof(CbFieldType) + Source.GetSize()); *static_cast<CbFieldType*>(Buffer.GetData()) = CbFieldTypeOps::GetType(GetType()); Buffer.RightChopInline(sizeof(CbFieldType)); @@ -1077,10 +1078,10 @@ void CbObjectView::CopyTo(MutableMemoryView Buffer) const { const MemoryView Source = GetPayloadView(); - ZEN_ASSERT(Buffer.GetSize() == (sizeof(CbFieldType) + Source.GetSize())); - // TEXT("Buffer is %" UINT64_FMT " bytes but %" UINT64_FMT " is required."), - // Buffer.GetSize(), - // sizeof(CbFieldType) + Source.GetSize()); + ZEN_ASSERT_FORMAT(Buffer.GetSize() == (sizeof(CbFieldType) + Source.GetSize()), + "Buffer is {} bytes but {} is required.", + Buffer.GetSize(), + sizeof(CbFieldType) + Source.GetSize()); *static_cast<CbFieldType*>(Buffer.GetData()) = CbFieldTypeOps::GetType(GetType()); Buffer.RightChopInline(sizeof(CbFieldType)); memcpy(Buffer.GetData(), Source.GetData(), Source.GetSize()); @@ -1151,10 +1152,10 @@ TCbFieldIterator<FieldType>::CopyRangeTo(MutableMemoryView InBuffer) const MemoryView Source; if (TryGetSerializedRangeView(Source)) { - ZEN_ASSERT(InBuffer.GetSize() == Source.GetSize()); - // TEXT("Buffer is %" UINT64_FMT " bytes but %" UINT64_FMT " is required."), - // InBuffer.GetSize(), - // Source.GetSize()); + ZEN_ASSERT_FORMAT(InBuffer.GetSize() == Source.GetSize(), + "Buffer is {} bytes but {} is required.", + InBuffer.GetSize(), + Source.GetSize()); memcpy(InBuffer.GetData(), Source.GetData(), Source.GetSize()); } else @@ -1654,7 +1655,7 @@ public: break; } default: - ZEN_ASSERT(false); + ZEN_ASSERT_FORMAT(false, "invalid field type: {}", uint8_t(Accessor.GetType())); break; } diff --git a/src/zencore/compactbinarybuilder.cpp b/src/zencore/compactbinarybuilder.cpp index d4ccd434d..5c08d2e6e 100644 --- a/src/zencore/compactbinarybuilder.cpp +++ b/src/zencore/compactbinarybuilder.cpp @@ -2,6 +2,7 @@ #include "zencore/compactbinarybuilder.h" +#include <zencore/assertfmt.h> #include <zencore/compactbinarypackage.h> #include <zencore/compactbinaryvalidation.h> #include <zencore/endian.h> @@ -128,13 +129,10 @@ CbWriter::Save() CbFieldViewIterator CbWriter::Save(const MutableMemoryView Buffer) { - ZEN_ASSERT(States.size() == 1 && States.back().Flags == StateFlags::None); - // TEXT("It is invalid to save while there are incomplete write operations.")); - ZEN_ASSERT(Data.size() > 0); // TEXT("It is invalid to save when nothing has been written.")); - ZEN_ASSERT(Buffer.GetSize() == Data.size()); - // TEXT("Buffer is %" UINT64_FMT " bytes but %" INT64_FMT " is required."), - // Buffer.GetSize(), - // Data.Num()); + ZEN_ASSERT_FORMAT(States.size() == 1 && States.back().Flags == StateFlags::None, + "It is invalid to save while there are incomplete write operations."); + ZEN_ASSERT_FORMAT(Data.size() > 0, "It is invalid to save when nothing has been written."); + ZEN_ASSERT_FORMAT(Buffer.GetSize() == Data.size(), "Buffer is {} bytes but {} is required.", Buffer.GetSize(), Data.size()); memcpy(Buffer.GetData(), Data.data(), Data.size()); return CbFieldViewIterator::MakeRange(Buffer); } @@ -142,9 +140,9 @@ CbWriter::Save(const MutableMemoryView Buffer) void CbWriter::Save(BinaryWriter& Writer) { - ZEN_ASSERT(States.size() == 1 && States.back().Flags == StateFlags::None); - // TEXT("It is invalid to save while there are incomplete write operations.")); - ZEN_ASSERT(Data.size() > 0); // TEXT("It is invalid to save when nothing has been written.")); + ZEN_ASSERT_FORMAT(States.size() == 1 && States.back().Flags == StateFlags::None, + "It is invalid to save while there are incomplete write operations."); + ZEN_ASSERT_FORMAT(Data.size() > 0, "It is invalid to save when nothing has been written."); Writer.Write(Data.data(), Data.size()); } @@ -166,10 +164,9 @@ CbWriter::BeginField() } else { - ZEN_ASSERT((State.Flags & StateFlags::Name) == StateFlags::Name); - // TEXT("A new field cannot be written until the previous field '%.*hs' is finished."), - // GetActiveName().Len(), - // GetActiveName().GetData()); + ZEN_ASSERT_FORMAT((State.Flags & StateFlags::Name) == StateFlags::Name, + "A new field cannot be written until the previous field '{}' is finished.", + GetActiveName()); } } @@ -184,8 +181,8 @@ CbWriter::EndField(CbFieldType Type) } else { - ZEN_ASSERT((State.Flags & StateFlags::Object) == StateFlags::None); - // TEXT("It is invalid to write an object field without a unique non-empty name.")); + ZEN_ASSERT((State.Flags & StateFlags::Object) == StateFlags::None, + "It is invalid to write an object field without a unique non-empty name."); } if (State.Count == 0) @@ -207,21 +204,18 @@ CbWriter& CbWriter::SetName(const std::string_view Name) { WriterState& State = States.back(); - ZEN_ASSERT((State.Flags & StateFlags::Array) != StateFlags::Array); - // TEXT("It is invalid to write a name for an array field. Name '%.*hs'"), - // Name.Len(), - // Name.GetData()); - ZEN_ASSERT(!Name.empty()); - // TEXT("%s"), - //(State.Flags & EStateFlags::Object) == EStateFlags::Object - // ? TEXT("It is invalid to write an empty name for an object field. Specify a unique non-empty name.") - // : TEXT("It is invalid to write an empty name for a top-level field. Specify a name or avoid this call.")); - ZEN_ASSERT((State.Flags & (StateFlags::Name | StateFlags::Field)) == StateFlags::None); - // TEXT("A new field '%.*hs' cannot be written until the previous field '%.*hs' is finished."), - // Name.Len(), - // Name.GetData(), - // GetActiveName().Len(), - // GetActiveName().GetData()); + ZEN_ASSERT_FORMAT((State.Flags & StateFlags::Array) != StateFlags::Array, + "It is invalid to write a name for an array field. Name '{}'", + Name); + ZEN_ASSERT_FORMAT(!Name.empty(), + "{}", + (State.Flags & StateFlags::Object) == StateFlags::Object + ? "It is invalid to write an empty name for an object field. Specify a unique non-empty name." + : "It is invalid to write an empty name for a top-level field. Specify a name or avoid this call."); + ZEN_ASSERT_FORMAT((State.Flags & (StateFlags::Name | StateFlags::Field)) == StateFlags::None, + "A new field '{}' cannot be written until the previous field '{}' is finished.", + Name, + GetActiveName()); BeginField(); State.Flags |= StateFlags::Name; @@ -296,7 +290,7 @@ CbWriter::MakeFieldsUniform(const int64_t FieldBeginOffset, const int64_t FieldE void CbWriter::AddField(const CbFieldView& Value) { - ZEN_ASSERT(Value.HasValue()); // , TEXT("It is invalid to write a field with no value.")); + ZEN_ASSERT_FORMAT(Value.HasValue(), "It is invalid to write a field with no value."); BeginField(); EndField(AppendCompactBinary(Value, Data)); } @@ -318,11 +312,10 @@ CbWriter::BeginObject() void CbWriter::EndObject() { - ZEN_ASSERT(States.size() > 1 && (States.back().Flags & StateFlags::Object) == StateFlags::Object); - - // TEXT("It is invalid to end an object when an object is not at the top of the stack.")); - ZEN_ASSERT((States.back().Flags & StateFlags::Field) == StateFlags::None); - // TEXT("It is invalid to end an object until the previous field is finished.")); + ZEN_ASSERT_FORMAT(States.size() > 1 && (States.back().Flags & StateFlags::Object) == StateFlags::Object, + "It is invalid to end an object when an object is not at the top of the stack."); + ZEN_ASSERT_FORMAT((States.back().Flags & StateFlags::Field) == StateFlags::None, + "It is invalid to end an object until the previous field is finished."); const bool bUniform = IsUniformType(States.back().UniformType); const uint64_t Count = States.back().Count; @@ -378,10 +371,10 @@ CbWriter::BeginArray() void CbWriter::EndArray() { - ZEN_ASSERT(States.size() > 1 && (States.back().Flags & StateFlags::Array) == StateFlags::Array); - // TEXT("Invalid attempt to end an array when an array is not at the top of the stack.")); - ZEN_ASSERT((States.back().Flags & StateFlags::Field) == StateFlags::None); - // TEXT("It is invalid to end an array until the previous field is finished.")); + ZEN_ASSERT_FORMAT(States.size() > 1 && (States.back().Flags & StateFlags::Array) == StateFlags::Array, + "Invalid attempt to end an array when an array is not at the top of the stack."); + ZEN_ASSERT_FORMAT((States.back().Flags & StateFlags::Field) == StateFlags::None, + "It is invalid to end an array until the previous field is finished."); const bool bUniform = IsUniformType(States.back().UniformType); const uint64_t Count = States.back().Count; States.pop_back(); diff --git a/src/zencore/filesystem.cpp b/src/zencore/filesystem.cpp index 06cda7382..29ec14e0c 100644 --- a/src/zencore/filesystem.cpp +++ b/src/zencore/filesystem.cpp @@ -7,6 +7,7 @@ #include <zencore/fmtutils.h> #include <zencore/iobuffer.h> #include <zencore/logging.h> +#include <zencore/process.h> #include <zencore/stream.h> #include <zencore/string.h> #include <zencore/testing.h> @@ -333,11 +334,17 @@ SupportsBlockRefCounting(std::filesystem::path Path) #endif // ZEN_PLATFORM_WINDOWS } -bool +static bool CloneFile(std::filesystem::path FromPath, std::filesystem::path ToPath) { #if ZEN_PLATFORM_WINDOWS - windows::Handle FromFile(CreateFileW(FromPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)); + windows::Handle FromFile(CreateFileW(FromPath.c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, + OPEN_EXISTING, + 0, + nullptr)); if (FromFile == INVALID_HANDLE_VALUE) { FromFile.Detach(); @@ -401,8 +408,10 @@ CloneFile(std::filesystem::path FromPath, std::filesystem::path ToPath) FILE_DISPOSITION_INFO FileDisposition = {TRUE}; if (!SetFileInformationByHandle(TargetFile, FileDispositionInfo, &FileDisposition, sizeof FileDisposition)) { + const DWORD ErrorCode = ::GetLastError(); TargetFile.Close(); DeleteFileW(ToPath.c_str()); + SetLastError(ErrorCode); return false; } @@ -531,6 +540,19 @@ CloneFile(std::filesystem::path FromPath, std::filesystem::path ToPath) #endif // ZEN_PLATFORM_WINDOWS } +void +CopyFile(std::filesystem::path FromPath, std::filesystem::path ToPath, const CopyFileOptions& Options, std::error_code& OutErrorCode) +{ + OutErrorCode.clear(); + + bool Success = CopyFile(FromPath, ToPath, Options); + + if (!Success) + { + OutErrorCode = MakeErrorCodeFromLastError(); + } +} + bool CopyFile(std::filesystem::path FromPath, std::filesystem::path ToPath, const CopyFileOptions& Options) { @@ -613,6 +635,133 @@ CopyFile(std::filesystem::path FromPath, std::filesystem::path ToPath, const Cop } void +CopyTree(std::filesystem::path FromPath, std::filesystem::path ToPath, const CopyFileOptions& Options) +{ + // Validate arguments + + if (FromPath.empty() || !std::filesystem::is_directory(FromPath)) + throw std::runtime_error("invalid CopyTree source directory specified"); + + if (ToPath.empty()) + throw std::runtime_error("no CopyTree target specified"); + + if (Options.MustClone && !SupportsBlockRefCounting(FromPath)) + throw std::runtime_error(fmt::format("cloning not possible from '{}'", FromPath)); + + if (std::filesystem::exists(ToPath)) + { + if (!std::filesystem::is_directory(ToPath)) + { + throw std::runtime_error(fmt::format("specified CopyTree target '{}' is not a directory", ToPath)); + } + } + else + { + std::filesystem::create_directories(ToPath); + } + + if (Options.MustClone && !SupportsBlockRefCounting(ToPath)) + throw std::runtime_error(fmt::format("cloning not possible from '{}'", ToPath)); + + // Verify source/target relationships + + std::error_code Ec; + std::filesystem::path FromCanonical = std::filesystem::canonical(FromPath, Ec); + + if (!Ec) + { + std::filesystem::path ToCanonical = std::filesystem::canonical(ToPath, Ec); + + if (!Ec) + { + if (FromCanonical == ToCanonical) + { + throw std::runtime_error("Target and source must be distinct files or directories"); + } + + if (ToCanonical.generic_string().starts_with(FromCanonical.generic_string()) || + FromCanonical.generic_string().starts_with(ToCanonical.generic_string())) + { + throw std::runtime_error("Invalid parent/child relationship for source/target directories"); + } + } + } + + struct CopyVisitor : public FileSystemTraversal::TreeVisitor + { + CopyVisitor(std::filesystem::path InBasePath, zen::CopyFileOptions InCopyOptions) : BasePath(InBasePath), CopyOptions(InCopyOptions) + { + } + + virtual void VisitFile(const std::filesystem::path& Parent, const path_view& File, uint64_t FileSize) override + { + std::error_code Ec; + const std::filesystem::path Relative = std::filesystem::relative(Parent, BasePath, Ec); + + if (Ec) + { + FailedFileCount++; + } + else + { + const std::filesystem::path FromPath = Parent / File; + std::filesystem::path ToPath; + + if (Relative.compare(".")) + { + zen::CreateDirectories(TargetPath / Relative); + + ToPath = TargetPath / Relative / File; + } + else + { + 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 (std::exception& Ex) + { + ++FailedFileCount; + + throw std::runtime_error(fmt::format("failed to copy '{}' to '{}': '{}'", FromPath, ToPath, Ex.what())); + } + } + } + + virtual bool VisitDirectory(const std::filesystem::path&, const path_view&) override { return true; } + + std::filesystem::path BasePath; + std::filesystem::path TargetPath; + zen::CopyFileOptions CopyOptions; + int FileCount = 0; + uint64_t ByteCount = 0; + int FailedFileCount = 0; + }; + + CopyVisitor Visitor{FromPath, Options}; + Visitor.TargetPath = ToPath; + + FileSystemTraversal Traversal; + Traversal.TraverseFileSystem(FromPath, Visitor); + + if (Visitor.FailedFileCount) + { + throw std::runtime_error(fmt::format("{} file copy operations FAILED", Visitor.FailedFileCount)); + } +} + +void WriteFile(std::filesystem::path Path, const IoBuffer* const* Data, size_t BufferCount) { #if ZEN_PLATFORM_WINDOWS @@ -1139,12 +1288,12 @@ PathFromHandle(void* NativeHandle, std::error_code& Ec) { if (NativeHandle == nullptr) { - return std::filesystem::path(); + return "<error handle 'nullptr'>"; } #if ZEN_PLATFORM_WINDOWS if (NativeHandle == INVALID_HANDLE_VALUE) { - return std::filesystem::path(); + return "<error handle 'invalid handle'>"; } auto GetFinalPathNameByHandleWRetry = @@ -1181,7 +1330,7 @@ PathFromHandle(void* NativeHandle, std::error_code& Ec) if (Error != ERROR_SUCCESS) { Ec = MakeErrorCodeFromLastError(); - return std::filesystem::path(); + return fmt::format("<error handle '{}'>", Ec.message()); } if (RequiredLengthIncludingNul < PathDataSize) @@ -1198,7 +1347,7 @@ PathFromHandle(void* NativeHandle, std::error_code& Ec) if (Error != ERROR_SUCCESS) { Ec = MakeErrorCodeFromLastError(); - return std::filesystem::path(); + return fmt::format("<error handle '{}'>", Ec.message()); } ZEN_UNUSED(FinalLength); return FullPath; @@ -1212,7 +1361,7 @@ PathFromHandle(void* NativeHandle, std::error_code& Ec) if (BytesRead <= 0) { Ec = MakeErrorCodeFromLastError(); - return {}; + return fmt::format("<error handle '{}'>", Ec.message()); } Link[BytesRead] = '\0'; @@ -1223,7 +1372,7 @@ PathFromHandle(void* NativeHandle, std::error_code& Ec) if (fcntl(Fd, F_GETPATH, Path) < 0) { Ec = MakeErrorCodeFromLastError(); - return {}; + return fmt::format("<error handle '{}'>", Ec.message()); } return Path; @@ -1460,6 +1609,76 @@ RotateFiles(const std::filesystem::path& Filename, std::size_t MaxFiles) return Result; } +std::error_code +RotateDirectories(const std::filesystem::path& DirectoryName, std::size_t MaxDirectories) +{ + const std::filesystem::path BasePath(DirectoryName.parent_path()); + const std::string Stem(DirectoryName.stem().string()); + + auto GetPathForIndex = [&](size_t Index) -> std::filesystem::path { + if (Index == 0) + { + return BasePath / Stem; + } + return BasePath / fmt::format("{}.{}", Stem, Index); + }; + + auto IsEmpty = [](const std::filesystem::path& Path, std::error_code& Ec) -> bool { return std::filesystem::is_empty(Path, Ec); }; + + std::error_code Result; + const bool BaseIsEmpty = IsEmpty(GetPathForIndex(0), Result); + if (Result) + { + return Result; + } + + if (BaseIsEmpty) + return Result; + + for (std::size_t i = MaxDirectories; i > 0; i--) + { + const std::filesystem::path SourcePath = GetPathForIndex(i - 1); + + if (std::filesystem::exists(SourcePath)) + { + std::filesystem::path TargetPath = GetPathForIndex(i); + + std::error_code DummyEc; + if (std::filesystem::exists(TargetPath, DummyEc)) + { + std::filesystem::remove_all(TargetPath, DummyEc); + } + std::filesystem::rename(SourcePath, TargetPath, DummyEc); + } + } + + return Result; +} + +std::filesystem::path +SearchPathForExecutable(std::string_view ExecutableName) +{ +#if ZEN_PLATFORM_WINDOWS + std::wstring Executable(Utf8ToWide(ExecutableName)); + + DWORD Result = SearchPathW(nullptr, Executable.c_str(), L".exe", 0, nullptr, nullptr); + + if (!Result) + return ExecutableName; + + auto PathBuffer = std::make_unique_for_overwrite<WCHAR[]>(Result); + + Result = SearchPathW(nullptr, Executable.c_str(), L".exe", Result, PathBuffer.get(), nullptr); + + if (!Result) + return ExecutableName; + + return PathBuffer.get(); +#else + return ExecutableName; +#endif +} + ////////////////////////////////////////////////////////////////////////// // // Testing related code follows... @@ -1619,6 +1838,55 @@ TEST_CASE("PathBuilder") # endif } +TEST_CASE("RotateDirectories") +{ + std::filesystem::path TestBaseDir = GetRunningExecutablePath().parent_path() / ".test"; + CleanDirectory(TestBaseDir); + std::filesystem::path RotateDir = TestBaseDir / "rotate_dir" / "dir_to_rotate"; + IoBuffer DummyFileData = IoBufferBuilder::MakeCloneFromMemory("blubb", 5); + + auto NewDir = [&] { + CreateDirectories(RotateDir); + WriteFile(RotateDir / ".placeholder", DummyFileData); + }; + + auto DirWithSuffix = [&](int Index) -> std::filesystem::path { return RotateDir.generic_string().append(fmt::format(".{}", Index)); }; + + const int RotateMax = 10; + + NewDir(); + CHECK(std::filesystem::exists(RotateDir)); + RotateDirectories(RotateDir, RotateMax); + CHECK(!std::filesystem::exists(RotateDir)); + CHECK(std::filesystem::exists(DirWithSuffix(1))); + NewDir(); + CHECK(std::filesystem::exists(RotateDir)); + RotateDirectories(RotateDir, RotateMax); + CHECK(!std::filesystem::exists(RotateDir)); + CHECK(std::filesystem::exists(DirWithSuffix(1))); + CHECK(std::filesystem::exists(DirWithSuffix(2))); + + for (int i = 0; i < RotateMax; ++i) + { + NewDir(); + std::error_code Ec = RotateDirectories(RotateDir, 10); + const bool IsError = !!Ec; + CHECK_EQ(IsError, false); + } + + CHECK(!std::filesystem::exists(RotateDir)); + + for (int i = 0; i < RotateMax; ++i) + { + CHECK(std::filesystem::exists(DirWithSuffix(i + 1))); + } + + for (int i = RotateMax; i < RotateMax + 5; ++i) + { + CHECK(!std::filesystem::exists(DirWithSuffix(RotateMax + i + 1))); + } +} + #endif } // namespace zen diff --git a/src/zencore/include/zencore/assertfmt.h b/src/zencore/include/zencore/assertfmt.h new file mode 100644 index 000000000..56383ffd9 --- /dev/null +++ b/src/zencore/include/zencore/assertfmt.h @@ -0,0 +1,48 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include <zencore/zencore.h> + +#include <fmt/args.h> +#include <string_view> + +namespace zen { + +namespace assert { + template<typename... T> + auto AssertCaptureArguments(T&&... Args) + { + return fmt::make_format_args(Args...); + } + + void ExecAssertFmt + [[noreturn]] (const char* Filename, int LineNumber, const char* FunctionName, std::string_view Format, fmt::format_args Args); + + // MSVC (v19.00.24215.1 at time of writing) ignores no-inline attributes on + // lambdas. This can be worked around by calling the lambda from inside this + // templated (and correctly non-inlined) function. + template<typename RetType = void, class InnerType, typename... ArgTypes> + RetType ZEN_FORCENOINLINE ZEN_DEBUG_SECTION CallColdNoInline(InnerType&& Inner, ArgTypes const&... Args) + { + return Inner(Args...); + } + +} // namespace assert + +#define ZEN_ASSERT_FORMAT(x, fmt, ...) \ + do \ + { \ + using namespace std::literals; \ + if (x) [[likely]] \ + break; \ + zen::assert::CallColdNoInline([&]() ZEN_FORCEINLINE { \ + zen::assert::ExecAssertFmt(__FILE__, \ + __LINE__, \ + __FUNCTION__, \ + "assert(" #x ") failed: " fmt ""sv, \ + zen::assert::AssertCaptureArguments(__VA_ARGS__)); \ + }); \ + } while (false) + +} // namespace zen diff --git a/src/zencore/include/zencore/blockingqueue.h b/src/zencore/include/zencore/blockingqueue.h index f92df5a54..e91fdc659 100644 --- a/src/zencore/include/zencore/blockingqueue.h +++ b/src/zencore/include/zencore/blockingqueue.h @@ -22,7 +22,6 @@ public: { std::lock_guard Lock(m_Lock); m_Queue.emplace_back(std::move(Item)); - m_Size++; } m_NewItemSignal.notify_one(); @@ -30,31 +29,33 @@ public: bool WaitAndDequeue(T& Item) { - if (m_CompleteAdding.load()) - { - return false; - } - std::unique_lock Lock(m_Lock); - m_NewItemSignal.wait(Lock, [this]() { return !m_Queue.empty() || m_CompleteAdding.load(); }); - - if (!m_Queue.empty()) + if (m_Queue.empty()) { - Item = std::move(m_Queue.front()); - m_Queue.pop_front(); - m_Size--; - - return true; + if (m_CompleteAdding) + { + return false; + } + m_NewItemSignal.wait(Lock, [this]() { return !m_Queue.empty() || m_CompleteAdding; }); + if (m_Queue.empty()) + { + ZEN_ASSERT(m_CompleteAdding); + return false; + } } - - return false; + Item = std::move(m_Queue.front()); + m_Queue.pop_front(); + return true; } void CompleteAdding() { - if (!m_CompleteAdding.load()) + std::unique_lock Lock(m_Lock); + if (!m_CompleteAdding) { - m_CompleteAdding.store(true); + m_CompleteAdding = true; + + Lock.unlock(); m_NewItemSignal.notify_all(); } } @@ -69,8 +70,7 @@ private: mutable std::mutex m_Lock; std::condition_variable m_NewItemSignal; std::deque<T> m_Queue; - std::atomic_bool m_CompleteAdding{false}; - std::atomic_uint32_t m_Size; + bool m_CompleteAdding = false; }; } // namespace zen diff --git a/src/zencore/include/zencore/compactbinarybuilder.h b/src/zencore/include/zencore/compactbinarybuilder.h index 89f69c1ab..9c81cf490 100644 --- a/src/zencore/include/zencore/compactbinarybuilder.h +++ b/src/zencore/include/zencore/compactbinarybuilder.h @@ -654,6 +654,24 @@ operator<<(CbWriter& Writer, const Oid& Value) ZENCORE_API CbWriter& operator<<(CbWriter& Writer, DateTime Value); ZENCORE_API CbWriter& operator<<(CbWriter& Writer, TimeSpan Value); +ZENCORE_API inline TimeSpan +ToTimeSpan(std::chrono::seconds Secs) +{ + return TimeSpan(0, 0, gsl::narrow<int>(Secs.count())); +}; +ZENCORE_API inline TimeSpan +ToTimeSpan(std::chrono::milliseconds MS) +{ + return TimeSpan(MS.count() * TimeSpan::TicksPerMillisecond); +} +ZENCORE_API inline DateTime +ToDateTime(std::chrono::system_clock::time_point TimePoint) +{ + time_t Time = std::chrono::system_clock::to_time_t(TimePoint); + tm UTCTime = *gmtime(&Time); + return DateTime(1900 + UTCTime.tm_year, UTCTime.tm_mon, UTCTime.tm_mday, UTCTime.tm_hour, UTCTime.tm_min, UTCTime.tm_sec); +} + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void usonbuilder_forcelink(); // internal diff --git a/src/zencore/include/zencore/filesystem.h b/src/zencore/include/zencore/filesystem.h index 22eb40e45..233941479 100644 --- a/src/zencore/include/zencore/filesystem.h +++ b/src/zencore/include/zencore/filesystem.h @@ -1,5 +1,4 @@ // Copyright Epic Games, Inc. All Rights Reserved. -// Copyright Epic Games, Inc. All Rights Reserved. #pragma once @@ -87,6 +86,11 @@ struct CopyFileOptions }; ZENCORE_API bool CopyFile(std::filesystem::path FromPath, std::filesystem::path ToPath, const CopyFileOptions& Options); +ZENCORE_API void CopyFile(std::filesystem::path FromPath, + std::filesystem::path ToPath, + const CopyFileOptions& Options, + std::error_code& OutError); +ZENCORE_API void CopyTree(std::filesystem::path FromPath, std::filesystem::path ToPath, const CopyFileOptions& Options); ZENCORE_API bool SupportsBlockRefCounting(std::filesystem::path Path); ZENCORE_API void PathToUtf8(const std::filesystem::path& Path, StringBuilderBase& Out); @@ -211,7 +215,10 @@ void GetDirectoryContent(const std::filesystem::path& RootDir, uint8_t Flags, Di std::string GetEnvVariable(std::string_view VariableName); +std::filesystem::path SearchPathForExecutable(std::string_view ExecutableName); + std::error_code RotateFiles(const std::filesystem::path& Filename, std::size_t MaxFiles); +std::error_code RotateDirectories(const std::filesystem::path& DirectoryName, std::size_t MaxDirectories); ////////////////////////////////////////////////////////////////////////// diff --git a/src/zencore/include/zencore/iobuffer.h b/src/zencore/include/zencore/iobuffer.h index 7accce41c..d891ed55b 100644 --- a/src/zencore/include/zencore/iobuffer.h +++ b/src/zencore/include/zencore/iobuffer.h @@ -31,6 +31,7 @@ enum class ZenContentType : uint8_t kCSS = 11, kPNG = 12, kIcon = 13, + kXML = 14, kCOUNT }; @@ -70,6 +71,8 @@ ToString(ZenContentType ContentType) return "png"sv; case ZenContentType::kIcon: return "icon"sv; + case ZenContentType::kXML: + return "xml"sv; } } diff --git a/src/zencore/include/zencore/logbase.h b/src/zencore/include/zencore/logbase.h index ad873aa51..00af68b0a 100644 --- a/src/zencore/include/zencore/logbase.h +++ b/src/zencore/include/zencore/logbase.h @@ -90,6 +90,9 @@ struct LoggerRef bool ShouldLog(int Level) const; inline operator bool() const { return SpdLogger != nullptr; } + void SetLogLevel(logging::level::LogLevel NewLogLevel); + logging::level::LogLevel GetLogLevel(); + spdlog::logger* SpdLogger = nullptr; }; diff --git a/src/zencore/include/zencore/logging.h b/src/zencore/include/zencore/logging.h index d14d1ab8d..6d44e31df 100644 --- a/src/zencore/include/zencore/logging.h +++ b/src/zencore/include/zencore/logging.h @@ -35,6 +35,10 @@ LoggerRef ErrorLog(); void SetErrorLog(std::string_view LoggerId); LoggerRef Get(std::string_view Name); +void ConfigureLogLevels(level::LogLevel Level, std::string_view Loggers); +void RefreshLogLevels(); +void RefreshLogLevels(level::LogLevel DefaultLevel); + struct LogCategory { inline LogCategory(std::string_view InCategory) : CategoryName(InCategory) {} @@ -235,6 +239,14 @@ std::string_view EmitActivitiesForLogging(StringBuilderBase& OutString); #define ZEN_LOG_SCOPE(...) ScopedLazyActivity $Activity##__LINE__([&](StringBuilderBase& Out) { Out << fmt::format(__VA_ARGS__); }) +#define ZEN_SCOPED_WARN(fmtstr, ...) \ + do \ + { \ + ExtendableStringBuilder<256> ScopeString; \ + const std::string_view Scopes = EmitActivitiesForLogging(ScopeString); \ + ZEN_LOG(Log(), zen::logging::level::Warn, fmtstr "{}", ##__VA_ARGS__, Scopes); \ + } while (false) + #define ZEN_SCOPED_ERROR(fmtstr, ...) \ do \ { \ diff --git a/src/zencore/include/zencore/process.h b/src/zencore/include/zencore/process.h new file mode 100644 index 000000000..d90a32301 --- /dev/null +++ b/src/zencore/include/zencore/process.h @@ -0,0 +1,98 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include <zencore/thread.h> +#include <zencore/zencore.h> + +#include <filesystem> + +namespace zen { + +/** Basic process abstraction + */ +class ProcessHandle +{ +public: + ZENCORE_API ProcessHandle(); + + ProcessHandle(const ProcessHandle&) = delete; + ProcessHandle& operator=(const ProcessHandle&) = delete; + + ZENCORE_API ~ProcessHandle(); + + ZENCORE_API void Initialize(int Pid); + ZENCORE_API void Initialize(void* ProcessHandle); /// Initialize with an existing handle - takes ownership of the handle + ZENCORE_API [[nodiscard]] bool IsRunning() const; + ZENCORE_API [[nodiscard]] bool IsValid() const; + ZENCORE_API bool Wait(int TimeoutMs = -1); + ZENCORE_API int WaitExitCode(); + ZENCORE_API void Terminate(int ExitCode); + ZENCORE_API void Reset(); + [[nodiscard]] inline int Pid() const { return m_Pid; } + +private: + void* m_ProcessHandle = nullptr; + int m_Pid = 0; +#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + int m_ExitCode = -1; +#endif +}; + +/** Basic process creation + */ +struct CreateProcOptions +{ + enum + { + Flag_NewConsole = 1 << 0, + Flag_Elevated = 1 << 1, + Flag_Unelevated = 1 << 2, + }; + + const std::filesystem::path* WorkingDirectory = nullptr; + uint32_t Flags = 0; + std::filesystem::path StdoutFile; +}; + +#if ZEN_PLATFORM_WINDOWS +using CreateProcResult = void*; // handle to the process +#else +using CreateProcResult = int32_t; // pid +#endif + +ZENCORE_API CreateProcResult CreateProc(const std::filesystem::path& Executable, + std::string_view CommandLine, // should also include arg[0] (executable name) + const CreateProcOptions& Options = {}); + +/** Process monitor - monitors a list of running processes via polling + + Intended to be used to monitor a set of "sponsor" processes, where + we need to determine when none of them remain alive + + */ + +class ProcessMonitor +{ +public: + ProcessMonitor(); + ~ProcessMonitor(); + + ZENCORE_API bool IsRunning(); + ZENCORE_API void AddPid(int Pid); + ZENCORE_API bool IsActive() const; + +private: + using HandleType = void*; + + mutable RwLock m_Lock; + std::vector<HandleType> m_ProcessHandles; +}; + +ZENCORE_API bool IsProcessRunning(int pid); +ZENCORE_API int GetCurrentProcessId(); +int GetProcessId(CreateProcResult ProcId); + +void process_forcelink(); // internal + +} // namespace zen diff --git a/src/zencore/include/zencore/string.h b/src/zencore/include/zencore/string.h index e3de2224c..3aec1647d 100644 --- a/src/zencore/include/zencore/string.h +++ b/src/zencore/include/zencore/string.h @@ -809,6 +809,19 @@ ForEachStrTok(const std::string_view& Str, char Delim, Fn&& Func) ////////////////////////////////////////////////////////////////////////// +inline std::string_view +ToString(bool Value) +{ + using namespace std::literals; + if (Value) + { + return "true"sv; + } + return "false"sv; +} + +////////////////////////////////////////////////////////////////////////// + inline int32_t StrCaseCompare(const char* Lhs, const char* Rhs, int64_t Length = -1) { diff --git a/src/zencore/include/zencore/thread.h b/src/zencore/include/zencore/thread.h index 9f2671610..2d0ef7396 100644 --- a/src/zencore/include/zencore/thread.h +++ b/src/zencore/include/zencore/thread.h @@ -10,6 +10,8 @@ #include <string_view> #include <vector> +#define ZEN_USE_WINDOWS_EVENTS ZEN_PLATFORM_WINDOWS + namespace zen { void SetCurrentThreadName(std::string_view ThreadName); @@ -107,7 +109,7 @@ public: ZENCORE_API bool Wait(int TimeoutMs = -1); ZENCORE_API void Close(); -#if ZEN_PLATFORM_WINDOWS +#if ZEN_USE_WINDOWS_EVENTS inline void* GetWindowsHandle() { return m_EventHandle; } #endif @@ -204,87 +206,7 @@ private: Event Complete; }; -/** Basic process abstraction - */ -class ProcessHandle -{ -public: - ZENCORE_API ProcessHandle(); - - ProcessHandle(const ProcessHandle&) = delete; - ProcessHandle& operator=(const ProcessHandle&) = delete; - - ZENCORE_API ~ProcessHandle(); - - ZENCORE_API void Initialize(int Pid); - ZENCORE_API void Initialize(void* ProcessHandle); /// Initialize with an existing handle - takes ownership of the handle - ZENCORE_API [[nodiscard]] bool IsRunning() const; - ZENCORE_API [[nodiscard]] bool IsValid() const; - ZENCORE_API bool Wait(int TimeoutMs = -1); - ZENCORE_API int WaitExitCode(); - ZENCORE_API void Terminate(int ExitCode); - ZENCORE_API void Reset(); - [[nodiscard]] inline int Pid() const { return m_Pid; } - -private: - void* m_ProcessHandle = nullptr; - int m_Pid = 0; -}; - -/** Basic process creation - */ -struct CreateProcOptions -{ - enum - { - Flag_NewConsole = 1 << 0, - Flag_Elevated = 1 << 1, - Flag_Unelevated = 1 << 2, - }; - - const std::filesystem::path* WorkingDirectory = nullptr; - uint32_t Flags = 0; -}; - -#if ZEN_PLATFORM_WINDOWS -using CreateProcResult = void*; // handle to the process -#else -using CreateProcResult = int32_t; // pid -#endif - -ZENCORE_API CreateProcResult CreateProc(const std::filesystem::path& Executable, - std::string_view CommandLine, // should also include arg[0] (executable name) - const CreateProcOptions& Options = {}); - -/** Process monitor - monitors a list of running processes via polling - - Intended to be used to monitor a set of "sponsor" processes, where - we need to determine when none of them remain alive - - */ - -class ProcessMonitor -{ -public: - ProcessMonitor(); - ~ProcessMonitor(); - - ZENCORE_API bool IsRunning(); - ZENCORE_API void AddPid(int Pid); - ZENCORE_API bool IsActive() const; - -private: - using HandleType = void*; - - mutable RwLock m_Lock; - std::vector<HandleType> m_ProcessHandles; -}; - -ZENCORE_API bool IsProcessRunning(int pid); -ZENCORE_API int GetCurrentProcessId(); ZENCORE_API int GetCurrentThreadId(); -int GetProcessId(CreateProcResult ProcId); - ZENCORE_API void Sleep(int ms); void thread_forcelink(); // internal diff --git a/src/zencore/include/zencore/trace.h b/src/zencore/include/zencore/trace.h index 665df5808..2d4c1e610 100644 --- a/src/zencore/include/zencore/trace.h +++ b/src/zencore/include/zencore/trace.h @@ -17,6 +17,7 @@ ZEN_THIRD_PARTY_INCLUDES_START 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) enum class TraceType { @@ -25,10 +26,10 @@ enum class TraceType None }; -void TraceInit(); +void TraceInit(std::string_view ProgramName); void TraceShutdown(); bool IsTracing(); -void TraceStart(const char* HostOrPath, TraceType Type); +void TraceStart(std::string_view ProgramName, const char* HostOrPath, TraceType Type); bool TraceStop(); #else diff --git a/src/zencore/include/zencore/windows.h b/src/zencore/include/zencore/windows.h index 0943a85ea..14026fef1 100644 --- a/src/zencore/include/zencore/windows.h +++ b/src/zencore/include/zencore/windows.h @@ -24,6 +24,7 @@ struct IUnknown; // Workaround for "combaseapi.h(229): error C2187: syntax erro # include <windows.h> # undef GetObject # undef SendMessage +# undef CopyFile ZEN_THIRD_PARTY_INCLUDES_END diff --git a/src/zencore/logging.cpp b/src/zencore/logging.cpp index 0d34372a9..90f4e2428 100644 --- a/src/zencore/logging.cpp +++ b/src/zencore/logging.cpp @@ -4,10 +4,14 @@ #include <zencore/string.h> #include <zencore/testing.h> +#include <zencore/thread.h> +ZEN_THIRD_PARTY_INCLUDES_START +#include <spdlog/details/registry.h> #include <spdlog/sinks/null_sink.h> #include <spdlog/sinks/stdout_color_sinks.h> #include <spdlog/spdlog.h> +ZEN_THIRD_PARTY_INCLUDES_END #if ZEN_PLATFORM_WINDOWS # pragma section(".zlog$a", read) @@ -46,6 +50,8 @@ LoggingContext::~LoggingContext() { } +////////////////////////////////////////////////////////////////////////// + static inline bool IsErrorLevel(int LogLevel) { @@ -176,8 +182,77 @@ ToStringView(level::LogLevel Level) } // namespace zen::logging::level +////////////////////////////////////////////////////////////////////////// + namespace zen::logging { +RwLock LogLevelsLock; +std::string LogLevels[level::LogLevelCount]; + +void +ConfigureLogLevels(level::LogLevel Level, std::string_view Loggers) +{ + RwLock::ExclusiveLockScope _(LogLevelsLock); + LogLevels[Level] = Loggers; +} + +void +RefreshLogLevels(level::LogLevel* DefaultLevel) +{ + spdlog::details::registry::log_levels Levels; + + { + RwLock::SharedLockScope _(LogLevelsLock); + + for (int i = 0; i < level::LogLevelCount; ++i) + { + level::LogLevel CurrentLevel{i}; + + std::string_view Spec = LogLevels[i]; + + while (!Spec.empty()) + { + std::string LoggerName; + + if (auto CommaPos = Spec.find_first_of(','); CommaPos != std::string_view::npos) + { + LoggerName = Spec.substr(CommaPos + 1); + Spec.remove_prefix(CommaPos + 1); + } + else + { + LoggerName = Spec; + Spec = {}; + } + + Levels[LoggerName] = to_spdlog_level(CurrentLevel); + } + } + } + + if (DefaultLevel) + { + spdlog::level::level_enum SpdDefaultLevel = to_spdlog_level(*DefaultLevel); + spdlog::details::registry::instance().set_levels(Levels, &SpdDefaultLevel); + } + else + { + spdlog::details::registry::instance().set_levels(Levels, nullptr); + } +} + +void +RefreshLogLevels(level::LogLevel DefaultLevel) +{ + RefreshLogLevels(&DefaultLevel); +} + +void +RefreshLogLevels() +{ + RefreshLogLevels(nullptr); +} + void SetLogLevel(level::LogLevel NewLogLevel) { @@ -240,6 +315,7 @@ Get(std::string_view Name) if (!Logger) { Logger = Default().SpdLogger->clone(std::string(Name)); + spdlog::apply_logger_env_levels(Logger); spdlog::register_logger(Logger); } @@ -252,6 +328,11 @@ std::shared_ptr<spdlog::logger> ConLogger; void SuppressConsoleLog() { + if (ConLogger) + { + spdlog::drop("console"); + ConLogger = {}; + } ConLogger = spdlog::null_logger_mt("console"); } @@ -262,6 +343,7 @@ ConsoleLog() if (!ConLogger) { ConLogger = spdlog::stdout_color_mt("console"); + spdlog::apply_logger_env_levels(ConLogger); ConLogger->set_pattern("%v"); } @@ -279,7 +361,6 @@ InitializeLogging() void ShutdownLogging() { - spdlog::drop_all(); spdlog::shutdown(); TheDefaultLogger = {}; } @@ -321,6 +402,18 @@ LoggerRef::ShouldLog(int Level) const return SpdLogger->should_log(static_cast<spdlog::level::level_enum>(Level)); } +void +LoggerRef::SetLogLevel(logging::level::LogLevel NewLogLevel) +{ + SpdLogger->set_level(to_spdlog_level(NewLogLevel)); +} + +logging::level::LogLevel +LoggerRef::GetLogLevel() +{ + return logging::level::to_logging_level(SpdLogger->level()); +} + thread_local ScopedActivityBase* t_ScopeStack = nullptr; ScopedActivityBase* diff --git a/src/zencore/process.cpp b/src/zencore/process.cpp new file mode 100644 index 000000000..2d0ec2de6 --- /dev/null +++ b/src/zencore/process.cpp @@ -0,0 +1,765 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zencore/process.h> + +#include <zencore/except.h> +#include <zencore/filesystem.h> +#include <zencore/fmtutils.h> +#include <zencore/scopeguard.h> +#include <zencore/string.h> +#include <zencore/testing.h> + +#include <thread> + +#if ZEN_PLATFORM_WINDOWS +# include <shellapi.h> +# include <Shlobj.h> +# include <zencore/windows.h> +#else +# include <fcntl.h> +# include <pthread.h> +# include <signal.h> +# include <sys/file.h> +# include <sys/sem.h> +# include <sys/stat.h> +# include <sys/syscall.h> +# include <sys/wait.h> +# include <time.h> +# include <unistd.h> +#endif + +ZEN_THIRD_PARTY_INCLUDES_START +#include <fmt/format.h> +ZEN_THIRD_PARTY_INCLUDES_END + +namespace zen { + +#if ZEN_PLATFORM_LINUX +const bool bNoZombieChildren = []() { + // When a child process exits it is put into a zombie state until the parent + // collects its result. This doesn't fit the Windows-like model that Zen uses + // where there is a less strict familial model and no zombification. Ignoring + // SIGCHLD signals removes the need to call wait() on zombies. Another option + // would be for the child to call setsid() but that would detatch the child + // from the terminal. + struct sigaction Action = {}; + sigemptyset(&Action.sa_mask); + Action.sa_handler = SIG_IGN; + sigaction(SIGCHLD, &Action, nullptr); + return true; +}(); +#endif + +ProcessHandle::ProcessHandle() = default; + +#if ZEN_PLATFORM_WINDOWS +void +ProcessHandle::Initialize(void* ProcessHandle) +{ + ZEN_ASSERT(m_ProcessHandle == nullptr); + + if (ProcessHandle == INVALID_HANDLE_VALUE) + { + ProcessHandle = nullptr; + } + + // TODO: perform some debug verification here to verify it's a valid handle? + m_ProcessHandle = ProcessHandle; + m_Pid = GetProcessId(m_ProcessHandle); +} +#endif // ZEN_PLATFORM_WINDOWS + +ProcessHandle::~ProcessHandle() +{ + Reset(); +} + +void +ProcessHandle::Initialize(int Pid) +{ + ZEN_ASSERT(m_ProcessHandle == nullptr); + +#if ZEN_PLATFORM_WINDOWS + m_ProcessHandle = OpenProcess(PROCESS_QUERY_INFORMATION | SYNCHRONIZE, FALSE, Pid); +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + if (Pid > 0) + { + m_ProcessHandle = (void*)(intptr_t(Pid)); + } +#endif + + if (!m_ProcessHandle) + { + ThrowLastError(fmt::format("ProcessHandle::Initialize(pid: {}) failed", Pid)); + } + + m_Pid = Pid; +} + +bool +ProcessHandle::IsRunning() const +{ + bool bActive = false; + +#if ZEN_PLATFORM_WINDOWS + DWORD ExitCode = 0; + GetExitCodeProcess(m_ProcessHandle, &ExitCode); + bActive = (ExitCode == STILL_ACTIVE); +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + bActive = (kill(pid_t(m_Pid), 0) == 0); +#endif + + return bActive; +} + +bool +ProcessHandle::IsValid() const +{ + return (m_ProcessHandle != nullptr); +} + +void +ProcessHandle::Terminate(int ExitCode) +{ + if (!IsRunning()) + { + return; + } + + bool bSuccess = false; + +#if ZEN_PLATFORM_WINDOWS + TerminateProcess(m_ProcessHandle, ExitCode); + DWORD WaitResult = WaitForSingleObject(m_ProcessHandle, INFINITE); + bSuccess = (WaitResult != WAIT_OBJECT_0); +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + ZEN_UNUSED(ExitCode); + bSuccess = (kill(m_Pid, SIGKILL) == 0); +#endif + + if (!bSuccess) + { + // What might go wrong here, and what is meaningful to act on? + } +} + +void +ProcessHandle::Reset() +{ + if (IsValid()) + { +#if ZEN_PLATFORM_WINDOWS + CloseHandle(m_ProcessHandle); +#endif + m_ProcessHandle = nullptr; + m_Pid = 0; + } +} + +bool +ProcessHandle::Wait(int TimeoutMs) +{ + using namespace std::literals; + +#if ZEN_PLATFORM_WINDOWS + const DWORD Timeout = (TimeoutMs < 0) ? INFINITE : TimeoutMs; + + const DWORD WaitResult = WaitForSingleObject(m_ProcessHandle, Timeout); + + switch (WaitResult) + { + case WAIT_OBJECT_0: + return true; + + case WAIT_TIMEOUT: + return false; + + case WAIT_FAILED: + break; + } +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + const int SleepMs = 20; + timespec SleepTime = {0, SleepMs * 1000 * 1000}; + for (int SleepedTimeMS = 0;; SleepedTimeMS += SleepMs) + { + int WaitState = 0; + waitpid(m_Pid, &WaitState, WNOHANG | WCONTINUED | WUNTRACED); + + if (WIFEXITED(WaitState)) + { + m_ExitCode = WEXITSTATUS(WaitState); + } + + if (kill(m_Pid, 0) < 0) + { + int32_t LastError = zen::GetLastError(); + if (LastError == ESRCH) + { + return true; + } + ThrowSystemError(static_cast<uint32_t>(LastError), "Process::Wait kill failed"sv); + } + + if (TimeoutMs >= 0 && SleepedTimeMS >= TimeoutMs) + { + return false; + } + + nanosleep(&SleepTime, nullptr); + } +#endif + + // What might go wrong here, and what is meaningful to act on? + ThrowLastError("Process::Wait failed"sv); +} + +int +ProcessHandle::WaitExitCode() +{ + Wait(-1); + +#if ZEN_PLATFORM_WINDOWS + DWORD ExitCode = 0; + GetExitCodeProcess(m_ProcessHandle, &ExitCode); + + ZEN_ASSERT(ExitCode != STILL_ACTIVE); + + return ExitCode; +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + return m_ExitCode; +#else + ZEN_NOT_IMPLEMENTED(); + + return 0; +#endif +} + +////////////////////////////////////////////////////////////////////////// + +#if !ZEN_PLATFORM_WINDOWS || ZEN_WITH_TESTS +static void +BuildArgV(std::vector<char*>& Out, char* CommandLine) +{ + char* Cursor = CommandLine; + while (true) + { + // Skip leading whitespace + for (; *Cursor == ' '; ++Cursor) + ; + + // Check for nullp terminator + if (*Cursor == '\0') + { + break; + } + + Out.push_back(Cursor); + + // Extract word + int QuoteCount = 0; + do + { + QuoteCount += (*Cursor == '\"'); + if (*Cursor == ' ' && !(QuoteCount & 1)) + { + break; + } + ++Cursor; + } while (*Cursor != '\0'); + + if (*Cursor == '\0') + { + break; + } + + *Cursor = '\0'; + ++Cursor; + } +} +#endif // !WINDOWS || TESTS + +#if ZEN_PLATFORM_WINDOWS +static CreateProcResult +CreateProcNormal(const std::filesystem::path& Executable, std::string_view CommandLine, const CreateProcOptions& Options) +{ + PROCESS_INFORMATION ProcessInfo{}; + STARTUPINFO StartupInfo{.cb = sizeof(STARTUPINFO)}; + + bool InheritHandles = false; + void* Environment = nullptr; + LPSECURITY_ATTRIBUTES ProcessAttributes = nullptr; + LPSECURITY_ATTRIBUTES ThreadAttributes = nullptr; + + DWORD CreationFlags = 0; + if (Options.Flags & CreateProcOptions::Flag_NewConsole) + { + CreationFlags |= CREATE_NEW_CONSOLE; + } + + const wchar_t* WorkingDir = nullptr; + if (Options.WorkingDirectory != nullptr) + { + WorkingDir = Options.WorkingDirectory->c_str(); + } + + ExtendableWideStringBuilder<256> CommandLineZ; + CommandLineZ << CommandLine; + + if (!Options.StdoutFile.empty()) + { + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof sa; + sa.lpSecurityDescriptor = nullptr; + sa.bInheritHandle = TRUE; + + StartupInfo.hStdInput = nullptr; + StartupInfo.hStdOutput = CreateFileW(Options.StdoutFile.c_str(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ, + &sa, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr); + + const BOOL Success = DuplicateHandle(GetCurrentProcess(), + StartupInfo.hStdOutput, + GetCurrentProcess(), + &StartupInfo.hStdError, + 0, + TRUE, + DUPLICATE_SAME_ACCESS); + + if (Success) + { + StartupInfo.dwFlags |= STARTF_USESTDHANDLES; + InheritHandles = true; + } + else + { + CloseHandle(StartupInfo.hStdOutput); + StartupInfo.hStdOutput = 0; + } + } + + BOOL Success = CreateProcessW(Executable.c_str(), + CommandLineZ.Data(), + ProcessAttributes, + ThreadAttributes, + InheritHandles, + CreationFlags, + Environment, + WorkingDir, + &StartupInfo, + &ProcessInfo); + + if (StartupInfo.dwFlags & STARTF_USESTDHANDLES) + { + CloseHandle(StartupInfo.hStdError); + CloseHandle(StartupInfo.hStdOutput); + } + + if (!Success) + { + return nullptr; + } + + CloseHandle(ProcessInfo.hThread); + return ProcessInfo.hProcess; +} + +static CreateProcResult +CreateProcUnelevated(const std::filesystem::path& Executable, std::string_view CommandLine, const CreateProcOptions& Options) +{ + /* Launches a binary with the shell as its parent. The shell (such as + Explorer) should be an unelevated process. */ + + // No sense in using this route if we are not elevated in the first place + if (IsUserAnAdmin() == FALSE) + { + return CreateProcNormal(Executable, CommandLine, Options); + } + + // Get the users' shell process and open it for process creation + HWND ShellWnd = GetShellWindow(); + if (ShellWnd == nullptr) + { + return nullptr; + } + + DWORD ShellPid; + GetWindowThreadProcessId(ShellWnd, &ShellPid); + + HANDLE Process = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, ShellPid); + if (Process == nullptr) + { + return nullptr; + } + auto $0 = MakeGuard([&] { CloseHandle(Process); }); + + // Creating a process as a child of another process is done by setting a + // thread-attribute list on the startup info passed to CreateProcess() + SIZE_T AttrListSize; + InitializeProcThreadAttributeList(nullptr, 1, 0, &AttrListSize); + + auto AttrList = (PPROC_THREAD_ATTRIBUTE_LIST)malloc(AttrListSize); + auto $1 = MakeGuard([&] { free(AttrList); }); + + if (!InitializeProcThreadAttributeList(AttrList, 1, 0, &AttrListSize)) + { + return nullptr; + } + + BOOL bOk = + UpdateProcThreadAttribute(AttrList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, (HANDLE*)&Process, sizeof(Process), nullptr, nullptr); + if (!bOk) + { + return nullptr; + } + + // By this point we know we are an elevated process. It is not allowed to + // create a process as a child of another unelevated process that share our + // elevated console window if we have one. So we'll need to create a new one. + uint32_t CreateProcFlags = EXTENDED_STARTUPINFO_PRESENT; + if (GetConsoleWindow() != nullptr) + { + CreateProcFlags |= CREATE_NEW_CONSOLE; + } + else + { + CreateProcFlags |= DETACHED_PROCESS; + } + + // Everything is set up now so we can proceed and launch the process + STARTUPINFOEXW StartupInfo = { + .StartupInfo = {.cb = sizeof(STARTUPINFOEXW)}, + .lpAttributeList = AttrList, + }; + PROCESS_INFORMATION ProcessInfo = {}; + + if (Options.Flags & CreateProcOptions::Flag_NewConsole) + { + CreateProcFlags |= CREATE_NEW_CONSOLE; + } + + ExtendableWideStringBuilder<256> CommandLineZ; + CommandLineZ << CommandLine; + + ExtendableWideStringBuilder<256> CurrentDirZ; + LPCWSTR WorkingDirectoryPtr = nullptr; + if (Options.WorkingDirectory) + { + CurrentDirZ << Options.WorkingDirectory->native(); + WorkingDirectoryPtr = CurrentDirZ.c_str(); + } + + bOk = CreateProcessW(Executable.c_str(), + CommandLineZ.Data(), + nullptr, + nullptr, + FALSE, + CreateProcFlags, + nullptr, + WorkingDirectoryPtr, + &StartupInfo.StartupInfo, + &ProcessInfo); + if (bOk == FALSE) + { + return nullptr; + } + + CloseHandle(ProcessInfo.hThread); + return ProcessInfo.hProcess; +} + +static CreateProcResult +CreateProcElevated(const std::filesystem::path& Executable, std::string_view CommandLine, const CreateProcOptions& Options) +{ + ExtendableWideStringBuilder<256> CommandLineZ; + CommandLineZ << CommandLine; + + SHELLEXECUTEINFO ShellExecuteInfo; + ZeroMemory(&ShellExecuteInfo, sizeof(ShellExecuteInfo)); + ShellExecuteInfo.cbSize = sizeof(ShellExecuteInfo); + ShellExecuteInfo.fMask = SEE_MASK_UNICODE | SEE_MASK_NOCLOSEPROCESS; + ShellExecuteInfo.lpFile = Executable.c_str(); + ShellExecuteInfo.lpVerb = TEXT("runas"); + ShellExecuteInfo.nShow = SW_SHOW; + ShellExecuteInfo.lpParameters = CommandLineZ.c_str(); + + if (Options.WorkingDirectory != nullptr) + { + ShellExecuteInfo.lpDirectory = Options.WorkingDirectory->c_str(); + } + + if (::ShellExecuteEx(&ShellExecuteInfo)) + { + return ShellExecuteInfo.hProcess; + } + + return nullptr; +} +#endif // ZEN_PLATFORM_WINDOWS + +CreateProcResult +CreateProc(const std::filesystem::path& Executable, std::string_view CommandLine, const CreateProcOptions& Options) +{ +#if ZEN_PLATFORM_WINDOWS + if (Options.Flags & CreateProcOptions::Flag_Unelevated) + { + return CreateProcUnelevated(Executable, CommandLine, Options); + } + + if (Options.Flags & CreateProcOptions::Flag_Elevated) + { + return CreateProcElevated(Executable, CommandLine, Options); + } + + return CreateProcNormal(Executable, CommandLine, Options); +#else + std::vector<char*> ArgV; + std::string CommandLineZ(CommandLine); + BuildArgV(ArgV, CommandLineZ.data()); + ArgV.push_back(nullptr); + + int ChildPid = fork(); + if (ChildPid < 0) + { + ThrowLastError("Failed to fork a new child process"); + } + else if (ChildPid == 0) + { + if (Options.WorkingDirectory != nullptr) + { + int Result = chdir(Options.WorkingDirectory->c_str()); + ZEN_UNUSED(Result); + } + + if (execv(Executable.c_str(), ArgV.data()) < 0) + { + ThrowLastError("Failed to exec() a new process image"); + } + } + + return ChildPid; +#endif +} + +////////////////////////////////////////////////////////////////////////// + +ProcessMonitor::ProcessMonitor() +{ +} + +ProcessMonitor::~ProcessMonitor() +{ + RwLock::ExclusiveLockScope _(m_Lock); + + for (HandleType& Proc : m_ProcessHandles) + { +#if ZEN_PLATFORM_WINDOWS + CloseHandle(Proc); +#endif + Proc = 0; + } +} + +bool +ProcessMonitor::IsRunning() +{ + RwLock::ExclusiveLockScope _(m_Lock); + + bool FoundOne = false; + + for (HandleType& Proc : m_ProcessHandles) + { + bool ProcIsActive; + +#if ZEN_PLATFORM_WINDOWS + DWORD ExitCode = 0; + GetExitCodeProcess(Proc, &ExitCode); + + ProcIsActive = (ExitCode == STILL_ACTIVE); + if (!ProcIsActive) + { + CloseHandle(Proc); + } +#else + int Pid = int(intptr_t(Proc)); + ProcIsActive = IsProcessRunning(Pid); +#endif + + if (!ProcIsActive) + { + Proc = 0; + } + + // Still alive + FoundOne |= ProcIsActive; + } + + std::erase_if(m_ProcessHandles, [](HandleType Handle) { return Handle == 0; }); + + return FoundOne; +} + +void +ProcessMonitor::AddPid(int Pid) +{ + HandleType ProcessHandle; + +#if ZEN_PLATFORM_WINDOWS + ProcessHandle = OpenProcess(PROCESS_QUERY_INFORMATION | SYNCHRONIZE, FALSE, Pid); +#else + ProcessHandle = HandleType(intptr_t(Pid)); +#endif + + if (ProcessHandle) + { + RwLock::ExclusiveLockScope _(m_Lock); + m_ProcessHandles.push_back(ProcessHandle); + } +} + +bool +ProcessMonitor::IsActive() const +{ + RwLock::SharedLockScope _(m_Lock); + return m_ProcessHandles.empty() == false; +} + +////////////////////////////////////////////////////////////////////////// + +bool +IsProcessRunning(int pid) +{ + // This function is arguably not super useful, a pid can be re-used + // by the OS so holding on to a pid and polling it over some time + // period will not necessarily tell you what you probably want to know. + +#if ZEN_PLATFORM_WINDOWS + HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + + if (!hProc) + { + DWORD Error = zen::GetLastError(); + + if (Error == ERROR_INVALID_PARAMETER) + { + return false; + } + + ThrowSystemError(Error, fmt::format("failed to open process with pid {}", pid)); + } + + bool bStillActive = true; + DWORD ExitCode = 0; + if (0 != GetExitCodeProcess(hProc, &ExitCode)) + { + bStillActive = ExitCode == STILL_ACTIVE; + } + else + { + ZEN_WARN("Unable to get exit code from handle for process '{}', treating the process as active", pid); + } + + CloseHandle(hProc); + + return bStillActive; +#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + return (kill(pid_t(pid), 0) == 0); +#endif +} + +int +GetCurrentProcessId() +{ +#if ZEN_PLATFORM_WINDOWS + return ::GetCurrentProcessId(); +#else + return int(getpid()); +#endif +} + +int +GetProcessId(CreateProcResult ProcId) +{ +#if ZEN_PLATFORM_WINDOWS + return static_cast<int>(::GetProcessId(ProcId)); +#else + return ProcId; +#endif +} + +#if ZEN_WITH_TESTS + +void +process_forcelink() +{ +} + +TEST_SUITE_BEGIN("core.process"); + +TEST_CASE("Process") +{ + int Pid = GetCurrentProcessId(); + CHECK(Pid > 0); + CHECK(IsProcessRunning(Pid)); +} + +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"}, + }; + + for (const auto& Case : Cases) + { + std::vector<char*> OutArgs; + StringBuilder<64> Mutable; + Mutable << Case.Input; + BuildArgV(OutArgs, Mutable.Data()); + + CHECK_EQ(OutArgs.size(), Case.WordCount); + + for (int i = 0, n = int(OutArgs.size()); i < n; ++i) + { + 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], '\"'); + } + } + } +} + +TEST_SUITE_END(/* core.process */); + +#endif + +} // namespace zen diff --git a/src/zencore/testing.cpp b/src/zencore/testing.cpp index 54d89ded2..936424e0f 100644 --- a/src/zencore/testing.cpp +++ b/src/zencore/testing.cpp @@ -5,12 +5,64 @@ #if ZEN_WITH_TESTS +# include <doctest/doctest.h> + namespace zen::testing { using namespace std::literals; +struct TestListener : public doctest::IReporter +{ + const std::string_view ColorYellow = "\033[0;33m"sv; + const std::string_view ColorNone = "\033[0m"sv; + + // constructor has to accept the ContextOptions by ref as a single argument + TestListener(const doctest::ContextOptions&) {} + + void report_query(const doctest::QueryData& /*in*/) override {} + + void test_run_start() override {} + + void test_run_end(const doctest::TestRunStats& /*in*/) override {} + + void test_case_start(const doctest::TestCaseData& in) override + { + Current = ∈ + ZEN_CONSOLE("{}======== TEST_CASE: {:<50} ========{}", ColorYellow, Current->m_name, ColorNone); + } + + // called when a test case is reentered because of unfinished subcases + void test_case_reenter(const doctest::TestCaseData& /*in*/) override + { + ZEN_CONSOLE("{}-------------------------------------------------------------------------------{}", ColorYellow, ColorNone); + } + + void test_case_end(const doctest::CurrentTestCaseStats& /*in*/) override { Current = nullptr; } + + void test_case_exception(const doctest::TestCaseException& /*in*/) override {} + + void subcase_start(const doctest::SubcaseSignature& in) override + { + ZEN_CONSOLE("{}-------- SUBCASE: {:<50} --------{}", + ColorYellow, + fmt::format("{}/{}", Current->m_name, in.m_name.c_str()), + ColorNone); + } + + void subcase_end() override {} + + void log_assert(const doctest::AssertData& /*in*/) override {} + + void log_message(const doctest::MessageData& /*in*/) override {} + + void test_case_skipped(const doctest::TestCaseData& /*in*/) override {} + + const doctest::TestCaseData* Current = nullptr; +}; + struct TestRunner::Impl { + Impl() { REGISTER_LISTENER("ZenTestListener", 1, TestListener); } doctest::Context Session; }; diff --git a/src/zencore/thread.cpp b/src/zencore/thread.cpp index 1f1b1b8f5..149a0d781 100644 --- a/src/zencore/thread.cpp +++ b/src/zencore/thread.cpp @@ -8,6 +8,9 @@ #include <zencore/scopeguard.h> #include <zencore/string.h> #include <zencore/testing.h> +#include <zencore/trace.h> + +#include <thread> #if ZEN_PLATFORM_LINUX # if !defined(_GNU_SOURCE) @@ -15,15 +18,15 @@ # endif #endif -#if ZEN_PLATFORM_WINDOWS -# include <shellapi.h> -# include <Shlobj.h> -# include <zencore/windows.h> -#else +#if !ZEN_USE_WINDOWS_EVENTS # include <chrono> # include <condition_variable> # include <mutex> +#endif +#if ZEN_PLATFORM_WINDOWS +# include <zencore/windows.h> +#else # include <fcntl.h> # include <pthread.h> # include <signal.h> @@ -36,10 +39,6 @@ # include <unistd.h> #endif -#include <zencore/trace.h> - -#include <thread> - ZEN_THIRD_PARTY_INCLUDES_START #include <fmt/format.h> ZEN_THIRD_PARTY_INCLUDES_END @@ -78,22 +77,6 @@ SetNameInternal(DWORD thread_id, const char* name) } #endif -#if ZEN_PLATFORM_LINUX -const bool bNoZombieChildren = []() { - // When a child process exits it is put into a zombie state until the parent - // collects its result. This doesn't fit the Windows-like model that Zen uses - // where there is a less strict familial model and no zombification. Ignoring - // SIGCHLD siganals removes the need to call wait() on zombies. Another option - // would be for the child to call setsid() but that would detatch the child - // from the terminal. - struct sigaction Action = {}; - sigemptyset(&Action.sa_mask); - Action.sa_handler = SIG_IGN; - sigaction(SIGCHLD, &Action, nullptr); - return true; -}(); -#endif - void SetCurrentThreadName([[maybe_unused]] std::string_view ThreadName) { @@ -152,12 +135,12 @@ RwLock::ReleaseExclusive() noexcept ////////////////////////////////////////////////////////////////////////// -#if !ZEN_PLATFORM_WINDOWS +#if !ZEN_USE_WINDOWS_EVENTS struct EventInner { std::mutex Mutex; std::condition_variable CondVar; - bool volatile bSet = false; + std::atomic_bool bSet{false}; }; #endif // !ZEN_PLATFORM_WINDOWS @@ -166,7 +149,7 @@ Event::Event() bool bManualReset = true; bool bInitialState = false; -#if ZEN_PLATFORM_WINDOWS +#if ZEN_USE_WINDOWS_EVENTS m_EventHandle = CreateEvent(nullptr, bManualReset, bInitialState, nullptr); #else ZEN_UNUSED(bManualReset); @@ -184,13 +167,13 @@ Event::~Event() void Event::Set() { -#if ZEN_PLATFORM_WINDOWS +#if ZEN_USE_WINDOWS_EVENTS SetEvent(m_EventHandle); #else auto* Inner = (EventInner*)m_EventHandle; { std::unique_lock Lock(Inner->Mutex); - Inner->bSet = true; + Inner->bSet.store(true); } Inner->CondVar.notify_all(); #endif @@ -199,13 +182,13 @@ Event::Set() void Event::Reset() { -#if ZEN_PLATFORM_WINDOWS +#if ZEN_USE_WINDOWS_EVENTS ResetEvent(m_EventHandle); #else auto* Inner = (EventInner*)m_EventHandle; { std::unique_lock Lock(Inner->Mutex); - Inner->bSet = false; + Inner->bSet.store(false); } #endif } @@ -213,10 +196,14 @@ Event::Reset() void Event::Close() { -#if ZEN_PLATFORM_WINDOWS +#if ZEN_USE_WINDOWS_EVENTS CloseHandle(m_EventHandle); #else auto* Inner = (EventInner*)m_EventHandle; + { + std::unique_lock Lock(Inner->Mutex); + Inner->bSet.store(true); + } delete Inner; #endif m_EventHandle = nullptr; @@ -225,7 +212,7 @@ Event::Close() bool Event::Wait(int TimeoutMs) { -#if ZEN_PLATFORM_WINDOWS +#if ZEN_USE_WINDOWS_EVENTS using namespace std::literals; const DWORD Timeout = (TimeoutMs < 0) ? INFINITE : TimeoutMs; @@ -239,25 +226,34 @@ Event::Wait(int TimeoutMs) return (Result == WAIT_OBJECT_0); #else - auto* Inner = (EventInner*)m_EventHandle; + auto* Inner = reinterpret_cast<EventInner*>(m_EventHandle); + + if (Inner->bSet.load()) + { + return true; + } if (TimeoutMs >= 0) { std::unique_lock Lock(Inner->Mutex); - if (Inner->bSet) + if (Inner->bSet.load()) { return true; } - return Inner->CondVar.wait_for(Lock, std::chrono::milliseconds(TimeoutMs), [&] { return Inner->bSet; }); + return Inner->CondVar.wait_for(Lock, std::chrono::milliseconds(TimeoutMs), [&] { return Inner->bSet.load(); }); } + // Infinite wait. This does not actually call the wait() function to work around + // an apparent issue in the underlying implementation. + std::unique_lock Lock(Inner->Mutex); - if (!Inner->bSet) + if (!Inner->bSet.load()) { - Inner->CondVar.wait(Lock, [&] { return Inner->bSet; }); + while (!Inner->CondVar.wait_for(Lock, std::chrono::milliseconds(1000), [&] { return Inner->bSet.load(); })) + ; } return true; @@ -398,9 +394,10 @@ NamedEvent::Wait(int TimeoutMs) } # if defined(_GNU_SOURCE) + const int TimeoutSec = TimeoutMs / 1000; struct timespec TimeoutValue = { - .tv_sec = TimeoutMs >> 10, - .tv_nsec = (TimeoutMs & 0x3ff) << 20, + .tv_sec = TimeoutSec, + .tv_nsec = (TimeoutMs - (TimeoutSec * 1000)) * 1000000, }; Result = semtimedop(Sem, &SemOp, 1, &TimeoutValue); # else @@ -418,7 +415,6 @@ NamedEvent::Wait(int TimeoutMs) TimeoutMs -= SleepTimeMs; } while (TimeoutMs > 0); # endif // _GNU_SOURCE - return Result == 0; #endif } @@ -520,582 +516,6 @@ NamedMutex::Exists(std::string_view MutexName) #endif // ZEN_PLATFORM_WINDOWS } -////////////////////////////////////////////////////////////////////////// - -ProcessHandle::ProcessHandle() = default; - -#if ZEN_PLATFORM_WINDOWS -void -ProcessHandle::Initialize(void* ProcessHandle) -{ - ZEN_ASSERT(m_ProcessHandle == nullptr); - - if (ProcessHandle == INVALID_HANDLE_VALUE) - { - ProcessHandle = nullptr; - } - - // TODO: perform some debug verification here to verify it's a valid handle? - m_ProcessHandle = ProcessHandle; - m_Pid = GetProcessId(m_ProcessHandle); -} -#endif // ZEN_PLATFORM_WINDOWS - -ProcessHandle::~ProcessHandle() -{ - Reset(); -} - -void -ProcessHandle::Initialize(int Pid) -{ - ZEN_ASSERT(m_ProcessHandle == nullptr); - -#if ZEN_PLATFORM_WINDOWS - m_ProcessHandle = OpenProcess(PROCESS_QUERY_INFORMATION | SYNCHRONIZE, FALSE, Pid); -#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC - if (Pid > 0) - { - m_ProcessHandle = (void*)(intptr_t(Pid)); - } -#endif - - if (!m_ProcessHandle) - { - ThrowLastError(fmt::format("ProcessHandle::Initialize(pid: {}) failed", Pid)); - } - - m_Pid = Pid; -} - -bool -ProcessHandle::IsRunning() const -{ - bool bActive = false; - -#if ZEN_PLATFORM_WINDOWS - DWORD ExitCode = 0; - GetExitCodeProcess(m_ProcessHandle, &ExitCode); - bActive = (ExitCode == STILL_ACTIVE); -#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC - bActive = (kill(pid_t(m_Pid), 0) == 0); -#endif - - return bActive; -} - -bool -ProcessHandle::IsValid() const -{ - return (m_ProcessHandle != nullptr); -} - -void -ProcessHandle::Terminate(int ExitCode) -{ - if (!IsRunning()) - { - return; - } - - bool bSuccess = false; - -#if ZEN_PLATFORM_WINDOWS - TerminateProcess(m_ProcessHandle, ExitCode); - DWORD WaitResult = WaitForSingleObject(m_ProcessHandle, INFINITE); - bSuccess = (WaitResult != WAIT_OBJECT_0); -#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC - ZEN_UNUSED(ExitCode); - bSuccess = (kill(m_Pid, SIGKILL) == 0); -#endif - - if (!bSuccess) - { - // What might go wrong here, and what is meaningful to act on? - } -} - -void -ProcessHandle::Reset() -{ - if (IsValid()) - { -#if ZEN_PLATFORM_WINDOWS - CloseHandle(m_ProcessHandle); -#endif - m_ProcessHandle = nullptr; - m_Pid = 0; - } -} - -bool -ProcessHandle::Wait(int TimeoutMs) -{ - using namespace std::literals; - -#if ZEN_PLATFORM_WINDOWS - const DWORD Timeout = (TimeoutMs < 0) ? INFINITE : TimeoutMs; - - const DWORD WaitResult = WaitForSingleObject(m_ProcessHandle, Timeout); - - switch (WaitResult) - { - case WAIT_OBJECT_0: - return true; - - case WAIT_TIMEOUT: - return false; - - case WAIT_FAILED: - break; - } -#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC - const int SleepMs = 20; - timespec SleepTime = {0, SleepMs * 1000 * 1000}; - for (int i = 0;; i += SleepMs) - { -# if ZEN_PLATFORM_MAC - int WaitState = 0; - waitpid(m_Pid, &WaitState, WNOHANG | WCONTINUED | WUNTRACED); -# endif - - if (kill(m_Pid, 0) < 0) - { - if (zen::GetLastError() == ESRCH) - { - return true; - } - break; - } - - if (TimeoutMs >= 0 && i >= TimeoutMs) - { - return false; - } - - nanosleep(&SleepTime, nullptr); - } -#endif - - // What might go wrong here, and what is meaningful to act on? - ThrowLastError("Process::Wait failed"sv); -} - -int -ProcessHandle::WaitExitCode() -{ - Wait(-1); - -#if ZEN_PLATFORM_WINDOWS - DWORD ExitCode = 0; - GetExitCodeProcess(m_ProcessHandle, &ExitCode); - - ZEN_ASSERT(ExitCode != STILL_ACTIVE); - - return ExitCode; -#else - ZEN_NOT_IMPLEMENTED(); - - return 0; -#endif -} - -////////////////////////////////////////////////////////////////////////// - -#if !ZEN_PLATFORM_WINDOWS || ZEN_WITH_TESTS -static void -BuildArgV(std::vector<char*>& Out, char* CommandLine) -{ - char* Cursor = CommandLine; - while (true) - { - // Skip leading whitespace - for (; *Cursor == ' '; ++Cursor) - ; - - // Check for nullp terminator - if (*Cursor == '\0') - { - break; - } - - Out.push_back(Cursor); - - // Extract word - int QuoteCount = 0; - do - { - QuoteCount += (*Cursor == '\"'); - if (*Cursor == ' ' && !(QuoteCount & 1)) - { - break; - } - ++Cursor; - } while (*Cursor != '\0'); - - if (*Cursor == '\0') - { - break; - } - - *Cursor = '\0'; - ++Cursor; - } -} -#endif // !WINDOWS || TESTS - -#if ZEN_PLATFORM_WINDOWS -static CreateProcResult -CreateProcNormal(const std::filesystem::path& Executable, std::string_view CommandLine, const CreateProcOptions& Options) -{ - PROCESS_INFORMATION ProcessInfo{}; - STARTUPINFO StartupInfo{.cb = sizeof(STARTUPINFO)}; - - const bool InheritHandles = false; - void* Environment = nullptr; - LPSECURITY_ATTRIBUTES ProcessAttributes = nullptr; - LPSECURITY_ATTRIBUTES ThreadAttributes = nullptr; - - DWORD CreationFlags = 0; - if (Options.Flags & CreateProcOptions::Flag_NewConsole) - { - CreationFlags |= CREATE_NEW_CONSOLE; - } - - const wchar_t* WorkingDir = nullptr; - if (Options.WorkingDirectory != nullptr) - { - WorkingDir = Options.WorkingDirectory->c_str(); - } - - ExtendableWideStringBuilder<256> CommandLineZ; - CommandLineZ << CommandLine; - - BOOL Success = CreateProcessW(Executable.c_str(), - CommandLineZ.Data(), - ProcessAttributes, - ThreadAttributes, - InheritHandles, - CreationFlags, - Environment, - WorkingDir, - &StartupInfo, - &ProcessInfo); - - if (!Success) - { - return nullptr; - } - - CloseHandle(ProcessInfo.hThread); - return ProcessInfo.hProcess; -} - -static CreateProcResult -CreateProcUnelevated(const std::filesystem::path& Executable, std::string_view CommandLine, const CreateProcOptions& Options) -{ - /* Launches a binary with the shell as its parent. The shell (such as - Explorer) should be an unelevated process. */ - - // No sense in using this route if we are not elevated in the first place - if (IsUserAnAdmin() == FALSE) - { - return CreateProcNormal(Executable, CommandLine, Options); - } - - // Get the users' shell process and open it for process creation - HWND ShellWnd = GetShellWindow(); - if (ShellWnd == nullptr) - { - return nullptr; - } - - DWORD ShellPid; - GetWindowThreadProcessId(ShellWnd, &ShellPid); - - HANDLE Process = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, ShellPid); - if (Process == nullptr) - { - return nullptr; - } - auto $0 = MakeGuard([&] { CloseHandle(Process); }); - - // Creating a process as a child of another process is done by setting a - // thread-attribute list on the startup info passed to CreateProcess() - SIZE_T AttrListSize; - InitializeProcThreadAttributeList(nullptr, 1, 0, &AttrListSize); - - auto AttrList = (PPROC_THREAD_ATTRIBUTE_LIST)malloc(AttrListSize); - auto $1 = MakeGuard([&] { free(AttrList); }); - - if (!InitializeProcThreadAttributeList(AttrList, 1, 0, &AttrListSize)) - { - return nullptr; - } - - BOOL bOk = - UpdateProcThreadAttribute(AttrList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, (HANDLE*)&Process, sizeof(Process), nullptr, nullptr); - if (!bOk) - { - return nullptr; - } - - // By this point we know we are an elevated process. It is not allowed to - // create a process as a child of another unelevated process that share our - // elevated console window if we have one. So we'll need to create a new one. - uint32_t CreateProcFlags = EXTENDED_STARTUPINFO_PRESENT; - if (GetConsoleWindow() != nullptr) - { - CreateProcFlags |= CREATE_NEW_CONSOLE; - } - else - { - CreateProcFlags |= DETACHED_PROCESS; - } - - // Everything is set up now so we can proceed and launch the process - STARTUPINFOEXW StartupInfo = { - .StartupInfo = {.cb = sizeof(STARTUPINFOEXW)}, - .lpAttributeList = AttrList, - }; - PROCESS_INFORMATION ProcessInfo = {}; - - if (Options.Flags & CreateProcOptions::Flag_NewConsole) - { - CreateProcFlags |= CREATE_NEW_CONSOLE; - } - - ExtendableWideStringBuilder<256> CommandLineZ; - CommandLineZ << CommandLine; - - bOk = CreateProcessW(Executable.c_str(), - CommandLineZ.Data(), - nullptr, - nullptr, - FALSE, - CreateProcFlags, - nullptr, - nullptr, - &StartupInfo.StartupInfo, - &ProcessInfo); - if (bOk == FALSE) - { - return nullptr; - } - - CloseHandle(ProcessInfo.hThread); - return ProcessInfo.hProcess; -} - -static CreateProcResult -CreateProcElevated(const std::filesystem::path& Executable, std::string_view CommandLine, const CreateProcOptions& Options) -{ - ExtendableWideStringBuilder<256> CommandLineZ; - CommandLineZ << CommandLine; - - SHELLEXECUTEINFO ShellExecuteInfo; - ZeroMemory(&ShellExecuteInfo, sizeof(ShellExecuteInfo)); - ShellExecuteInfo.cbSize = sizeof(ShellExecuteInfo); - ShellExecuteInfo.fMask = SEE_MASK_UNICODE | SEE_MASK_NOCLOSEPROCESS; - ShellExecuteInfo.lpFile = Executable.c_str(); - ShellExecuteInfo.lpVerb = TEXT("runas"); - ShellExecuteInfo.nShow = SW_SHOW; - ShellExecuteInfo.lpParameters = CommandLineZ.c_str(); - - if (Options.WorkingDirectory != nullptr) - { - ShellExecuteInfo.lpDirectory = Options.WorkingDirectory->c_str(); - } - - if (::ShellExecuteEx(&ShellExecuteInfo)) - { - return ShellExecuteInfo.hProcess; - } - - return nullptr; -} -#endif // ZEN_PLATFORM_WINDOWS - -CreateProcResult -CreateProc(const std::filesystem::path& Executable, std::string_view CommandLine, const CreateProcOptions& Options) -{ -#if ZEN_PLATFORM_WINDOWS - if (Options.Flags & CreateProcOptions::Flag_Unelevated) - { - return CreateProcUnelevated(Executable, CommandLine, Options); - } - - if (Options.Flags & CreateProcOptions::Flag_Elevated) - { - return CreateProcElevated(Executable, CommandLine, Options); - } - - return CreateProcNormal(Executable, CommandLine, Options); -#else - std::vector<char*> ArgV; - std::string CommandLineZ(CommandLine); - BuildArgV(ArgV, CommandLineZ.data()); - ArgV.push_back(nullptr); - - int ChildPid = fork(); - if (ChildPid < 0) - { - ThrowLastError("Failed to fork a new child process"); - } - else if (ChildPid == 0) - { - if (Options.WorkingDirectory != nullptr) - { - int Result = chdir(Options.WorkingDirectory->c_str()); - ZEN_UNUSED(Result); - } - - if (execv(Executable.c_str(), ArgV.data()) < 0) - { - ThrowLastError("Failed to exec() a new process image"); - } - } - - return ChildPid; -#endif -} - -////////////////////////////////////////////////////////////////////////// - -ProcessMonitor::ProcessMonitor() -{ -} - -ProcessMonitor::~ProcessMonitor() -{ - RwLock::ExclusiveLockScope _(m_Lock); - - for (HandleType& Proc : m_ProcessHandles) - { -#if ZEN_PLATFORM_WINDOWS - CloseHandle(Proc); -#endif - Proc = 0; - } -} - -bool -ProcessMonitor::IsRunning() -{ - RwLock::ExclusiveLockScope _(m_Lock); - - bool FoundOne = false; - - for (HandleType& Proc : m_ProcessHandles) - { - bool ProcIsActive; - -#if ZEN_PLATFORM_WINDOWS - DWORD ExitCode = 0; - GetExitCodeProcess(Proc, &ExitCode); - - ProcIsActive = (ExitCode == STILL_ACTIVE); - if (!ProcIsActive) - { - CloseHandle(Proc); - } -#else - int Pid = int(intptr_t(Proc)); - ProcIsActive = IsProcessRunning(Pid); -#endif - - if (!ProcIsActive) - { - Proc = 0; - } - - // Still alive - FoundOne |= ProcIsActive; - } - - std::erase_if(m_ProcessHandles, [](HandleType Handle) { return Handle == 0; }); - - return FoundOne; -} - -void -ProcessMonitor::AddPid(int Pid) -{ - HandleType ProcessHandle; - -#if ZEN_PLATFORM_WINDOWS - ProcessHandle = OpenProcess(PROCESS_QUERY_INFORMATION | SYNCHRONIZE, FALSE, Pid); -#else - ProcessHandle = HandleType(intptr_t(Pid)); -#endif - - if (ProcessHandle) - { - RwLock::ExclusiveLockScope _(m_Lock); - m_ProcessHandles.push_back(ProcessHandle); - } -} - -bool -ProcessMonitor::IsActive() const -{ - RwLock::SharedLockScope _(m_Lock); - return m_ProcessHandles.empty() == false; -} - -////////////////////////////////////////////////////////////////////////// - -bool -IsProcessRunning(int pid) -{ - // This function is arguably not super useful, a pid can be re-used - // by the OS so holding on to a pid and polling it over some time - // period will not necessarily tell you what you probably want to know. - -#if ZEN_PLATFORM_WINDOWS - HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); - - if (!hProc) - { - DWORD Error = zen::GetLastError(); - - if (Error == ERROR_INVALID_PARAMETER) - { - return false; - } - - ThrowSystemError(Error, fmt::format("failed to open process with pid {}", pid)); - } - - bool bStillActive = true; - DWORD ExitCode = 0; - if (0 != GetExitCodeProcess(hProc, &ExitCode)) - { - bStillActive = ExitCode == STILL_ACTIVE; - } - else - { - ZEN_WARN("Unable to get exit code from handle for process '{}', treating the process as active", pid); - } - - CloseHandle(hProc); - - return bStillActive; -#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC - return (kill(pid_t(pid), 0) == 0); -#endif -} - -int -GetCurrentProcessId() -{ -#if ZEN_PLATFORM_WINDOWS - return ::GetCurrentProcessId(); -#else - return int(getpid()); -#endif -} - int GetCurrentThreadId() { @@ -1108,16 +528,6 @@ GetCurrentThreadId() #endif } -int -GetProcessId(CreateProcResult ProcId) -{ -#if ZEN_PLATFORM_WINDOWS - return static_cast<int>(::GetProcessId(ProcId)); -#else - return ProcId; -#endif -} - void Sleep(int ms) { @@ -1140,65 +550,11 @@ thread_forcelink() { } -TEST_CASE("Thread") -{ - int Pid = GetCurrentProcessId(); - CHECK(Pid > 0); - CHECK(IsProcessRunning(Pid)); - - CHECK_FALSE(GetCurrentThreadId() == 0); -} +TEST_SUITE_BEGIN("core.thread"); -TEST_CASE("BuildArgV") +TEST_CASE("GetCurrentThreadId") { - 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"}, - }; - - for (const auto& Case : Cases) - { - std::vector<char*> OutArgs; - StringBuilder<64> Mutable; - Mutable << Case.Input; - BuildArgV(OutArgs, Mutable.Data()); - - CHECK_EQ(OutArgs.size(), Case.WordCount); - - for (int i = 0, n = int(OutArgs.size()); i < n; ++i) - { - 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_FALSE(GetCurrentThreadId() == 0); } TEST_CASE("NamedEvent") @@ -1213,19 +569,20 @@ TEST_CASE("NamedEvent") CHECK(!bEventSet); } + NamedEvent ReadyEvent(Name + "_ready"); + // Thread check std::thread Waiter = std::thread([Name]() { NamedEvent ReadyEvent(Name + "_ready"); ReadyEvent.Set(); NamedEvent TestEvent(Name); - TestEvent.Wait(100); + TestEvent.Wait(1000); }); - NamedEvent ReadyEvent(Name + "_ready"); ReadyEvent.Wait(); - zen::Sleep(50); + zen::Sleep(100); TestEvent.Set(); Waiter.join(); @@ -1253,6 +610,8 @@ TEST_CASE("NamedMutex") CHECK(!NamedMutex::Exists(Name)); } +TEST_SUITE_END(); + #endif // ZEN_WITH_TESTS } // namespace zen diff --git a/src/zencore/trace.cpp b/src/zencore/trace.cpp index d71ca0984..f7e4c4b68 100644 --- a/src/zencore/trace.cpp +++ b/src/zencore/trace.cpp @@ -9,7 +9,7 @@ # include <zencore/trace.h> void -TraceInit() +TraceInit(std::string_view ProgramName) { static std::atomic_bool gInited = false; bool Expected = false; @@ -23,14 +23,20 @@ TraceInit() }; trace::Initialize(Desc); +# if ZEN_PLATFORM_WINDOWS + const char* CommandLineString = GetCommandLineA(); +# else + const char* CommandLineString = ""; +# endif + trace::ThreadRegister("main", /* system id */ 0, /* sort id */ 0); - trace::DescribeSession("zenserver", + trace::DescribeSession(ProgramName, # if ZEN_BUILD_DEBUG trace::Build::Debug, # else trace::Build::Development, # endif - "", + CommandLineString, ZEN_CFG_VERSION_BUILD_STRING); } @@ -48,9 +54,9 @@ IsTracing() } void -TraceStart(const char* HostOrPath, TraceType Type) +TraceStart(std::string_view ProgramName, const char* HostOrPath, TraceType Type) { - TraceInit(); + TraceInit(ProgramName); switch (Type) { case TraceType::Network: diff --git a/src/zencore/workthreadpool.cpp b/src/zencore/workthreadpool.cpp index 3a4b1e6a1..6ff6463dd 100644 --- a/src/zencore/workthreadpool.cpp +++ b/src/zencore/workthreadpool.cpp @@ -74,6 +74,7 @@ struct WorkerThreadPool::Impl { WaitForThreadpoolWorkCallbacks(m_Work, /* CancelPendingCallbacks */ TRUE); CloseThreadpoolWork(m_Work); + CloseThreadpool(m_ThreadPool); } void ScheduleWork(Ref<IWork> Work) @@ -109,6 +110,7 @@ struct WorkerThreadPool::Impl m_WorkQueue.pop_front(); } + ZEN_TRACE_CPU_FLUSH("AsyncWork"); WorkFromQueue->Execute(); } }; @@ -150,7 +152,10 @@ struct WorkerThreadPool::Impl for (std::thread& Thread : m_WorkerThreads) { - Thread.join(); + if (Thread.joinable()) + { + Thread.join(); + } } m_WorkerThreads.clear(); @@ -174,6 +179,7 @@ WorkerThreadPool::Impl::WorkerThreadFunction(ThreadStartInfo Info) { try { + ZEN_TRACE_CPU_FLUSH("AsyncWork"); Work->Execute(); } catch (std::exception& e) @@ -219,7 +225,17 @@ WorkerThreadPool::ScheduleWork(Ref<IWork> Work) } else { - Work->Execute(); + try + { + ZEN_TRACE_CPU_FLUSH("SyncWork"); + Work->Execute(); + } + catch (std::exception& e) + { + Work->m_Exception = std::current_exception(); + + ZEN_WARN("Caught exception when executing worker synchronously: {}", e.what()); + } } } diff --git a/src/zencore/zencore.cpp b/src/zencore/zencore.cpp index 9377a733b..eed903f54 100644 --- a/src/zencore/zencore.cpp +++ b/src/zencore/zencore.cpp @@ -10,6 +10,7 @@ # include <pthread.h> #endif +#include <zencore/assertfmt.h> #include <zencore/blake3.h> #include <zencore/compactbinary.h> #include <zencore/compactbinarybuilder.h> @@ -24,6 +25,7 @@ #include <zencore/logging.h> #include <zencore/memory.h> #include <zencore/mpscqueue.h> +#include <zencore/process.h> #include <zencore/sha1.h> #include <zencore/stats.h> #include <zencore/stream.h> @@ -33,6 +35,22 @@ #include <zencore/uid.h> #include <zencore/workthreadpool.h> +#include <fmt/format.h> + +namespace zen::assert { + +void +ExecAssertFmt(const char* Filename, int LineNumber, const char* FunctionName, std::string_view Format, fmt::format_args Args) +{ + fmt::basic_memory_buffer<char, 1024> Message; + fmt::vformat_to(fmt::appender(Message), Format, Args); + Message.push_back('\0'); + + AssertImpl::ExecAssert(Filename, LineNumber, FunctionName, Message.data()); +} + +} // namespace zen::assert + namespace zen { void refcount_forcelink(); @@ -123,6 +141,7 @@ zencore_forcelinktests() zen::logging_forcelink(); zen::memory_forcelink(); zen::mpscqueue_forcelink(); + zen::process_forcelink(); zen::refcount_forcelink(); zen::sha1_forcelink(); zen::stats_forcelink(); @@ -142,6 +161,8 @@ zencore_forcelinktests() namespace zen { +TEST_SUITE_BEGIN("core.assert"); + TEST_CASE("Assert.Default") { bool A = true; @@ -149,6 +170,13 @@ TEST_CASE("Assert.Default") CHECK_THROWS_WITH(ZEN_ASSERT(A == B), "A == B"); } +TEST_CASE("Assert.Format") +{ + bool A = true; + bool B = false; + CHECK_THROWS_WITH(ZEN_ASSERT_FORMAT(A == B, "{} == {}", A, B), "assert(A == B) failed: true == false"); +} + TEST_CASE("Assert.Custom") { struct MyAssertImpl : AssertImpl @@ -183,6 +211,7 @@ TEST_CASE("Assert.Custom") CHECK(strcmp(MyAssert.Message, "A == B") == 0); } +TEST_SUITE_END(); #endif } // namespace zen |