diff --git a/docs/windows-driver.md b/docs/windows-driver.md index b0c8b70..da419fd 100644 --- a/docs/windows-driver.md +++ b/docs/windows-driver.md @@ -202,12 +202,24 @@ and returns purchase and account-management URLs with the status. Applications must treat activation keys as transient secrets and must not persist or log them. -Development driver builds write a lightweight UMDF trace to: +### Driver Diagnostic Logs + +The UMDF driver writes lifecycle events and operational failures to the +following path (normally `C:\Windows\Temp`): ```text -C:\Windows\Temp\libvirtualhid-umdf-driver.log +%WINDIR%\Temp\libvirtualhid-umdf-driver.log ``` +Successful input reports are deliberately excluded because they are the +latency-sensitive hot path. When the active log would exceed 5 MiB, the driver +rotates it before writing the next entry. Five previous logs are retained as +`libvirtualhid-umdf-driver.log.1` through +`libvirtualhid-umdf-driver.log.5`; `.1` is the newest backup. The active log +and all numbered backups use at most approximately 30 MiB in total. Include +the active log and any numbered backups when reporting a driver installation, +device-lifecycle, authorization, or input-submission problem. + During rapid development reinstalls, the fixed global control symbolic link can briefly outlive the previous root device. The driver treats that collision as non-fatal, and normal clients discover the PnP control device interface first. diff --git a/src/platform/windows/driver/CMakeLists.txt b/src/platform/windows/driver/CMakeLists.txt index d4ffd1f..a9f7cbb 100644 --- a/src/platform/windows/driver/CMakeLists.txt +++ b/src/platform/windows/driver/CMakeLists.txt @@ -195,7 +195,8 @@ configure_file( @ONLY) add_library(libvirtualhid_umdf SHARED - "${CMAKE_CURRENT_SOURCE_DIR}/libvirtualhid_umdf.cpp") + "${CMAKE_CURRENT_SOURCE_DIR}/libvirtualhid_umdf.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/rotating_trace_log.cpp") target_include_directories(libvirtualhid_umdf PRIVATE diff --git a/src/platform/windows/driver/libvirtualhid_umdf.cpp b/src/platform/windows/driver/libvirtualhid_umdf.cpp index 10b38e8..7406bf1 100644 --- a/src/platform/windows/driver/libvirtualhid_umdf.cpp +++ b/src/platform/windows/driver/libvirtualhid_umdf.cpp @@ -39,7 +39,6 @@ #include #include #include -#include #include #include #include @@ -48,14 +47,15 @@ #include #include #include -#include #include // local includes #include "generic_pid_protocol.hpp" #include "lvh_windows_protocol.h" #include "playstation_feature_protocol.hpp" +#include "rotating_trace_log.hpp" #include "switch_pro_protocol.hpp" +#include "unique_win32_handle.hpp" #include "windows_device_identity.hpp" using VhfContext = PVOID; // NOSONAR(cpp:S5008): VHF callback ABI requires PVOID; client context narrows to DeviceRecord. @@ -84,47 +84,6 @@ namespace { std::remove_pointer_t, decltype(&::CloseServiceHandle)>; - class UniqueHandle { - public: - explicit UniqueHandle(HANDLE handle = nullptr): - handle_ {handle} {} - - UniqueHandle(const UniqueHandle &) = delete; - UniqueHandle &operator=(const UniqueHandle &) = delete; - - UniqueHandle(UniqueHandle &&other) noexcept: - handle_ {std::exchange(other.handle_, nullptr)} {} - - UniqueHandle &operator=(UniqueHandle &&other) noexcept { - if (this != &other) { - reset(std::exchange(other.handle_, nullptr)); - } - return *this; - } - - ~UniqueHandle() { - reset(); - } - - HANDLE get() const { - return handle_; - } - - explicit operator bool() const { - return handle_ != nullptr && handle_ != INVALID_HANDLE_VALUE; - } - - void reset(HANDLE handle = nullptr) { - if (handle_ != nullptr && handle_ != INVALID_HANDLE_VALUE) { - static_cast(CloseHandle(handle_)); - } - handle_ = handle; - } - - private: - HANDLE handle_ {}; - }; - struct DeviceRecord { std::mutex mutex; std::uint64_t driver_device_id {}; @@ -183,19 +142,6 @@ namespace { trace_file_path.append(trace_directory); trace_file_path.append(trace_file_name); - const auto file = CreateFileW( - trace_file_path.c_str(), - FILE_APPEND_DATA, - FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - nullptr, - OPEN_ALWAYS, - FILE_ATTRIBUTE_NORMAL, - nullptr - ); - if (file == INVALID_HANDLE_VALUE) { - return; - } - SYSTEMTIME time {}; GetSystemTime(&time); @@ -212,12 +158,7 @@ namespace { step, static_cast(status) ); - DWORD bytes_written {}; - const auto bytes_to_write = - static_cast(std::min(line.size(), static_cast(std::numeric_limits::max()))); - static_cast(WriteFile(file, line.data(), bytes_to_write, &bytes_written, nullptr)); - - static_cast(CloseHandle(file)); + static_cast(lvh::detail::windows::append_rotating_trace_log(trace_file_path, line)); } bool valid_header(std::uint32_t version, std::uint32_t size, std::uint32_t expected_size) { @@ -670,9 +611,9 @@ namespace { return false; } - auto process = UniqueHandle { + auto process = lvh::detail::windows::make_unique_win32_handle( OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, requestor_process_id) - }; + ); if (!process) { trace_status("broker service identity: open requestor process failed"); return requestor_is_running_broker_service(requestor_process_id); @@ -684,7 +625,7 @@ namespace { return requestor_is_running_broker_service(requestor_process_id); } - const auto token = UniqueHandle {token_handle}; + const auto token = lvh::detail::windows::make_unique_win32_handle(token_handle); const auto service_sid = broker_service_sid(); if (service_sid && token_has_sid(token.get(), *service_sid)) { return true; @@ -786,7 +727,10 @@ namespace { if (record.vhf_handle == nullptr) { return; } - trace_status("switch_pro_reply VhfReadReportSubmit", VhfReadReportSubmit(record.vhf_handle, &packet)); + const auto submit_status = VhfReadReportSubmit(record.vhf_handle, &packet); + if (!NT_SUCCESS(submit_status)) { + trace_status("switch_pro_reply VhfReadReportSubmit", submit_status); + } } LvhWindowsOutputReportEvent make_output_event(DeviceRecord &record, const HID_XFER_PACKET &packet) { @@ -989,8 +933,6 @@ namespace { return; } - trace_status("submit_input_report begin"); - auto record = find_device(submit_request->driver_device_id); if (!record) { trace_status("submit_input_report missing device"); @@ -1024,7 +966,9 @@ namespace { packet.reportId = record->request.hardware_ids.report_id; const auto submit_status = VhfReadReportSubmit(record->vhf_handle, &packet); - trace_status("submit_input_report VhfReadReportSubmit", submit_status); + if (!NT_SUCCESS(submit_status)) { + trace_status("submit_input_report VhfReadReportSubmit", submit_status); + } complete_request(request, submit_status); } @@ -1181,7 +1125,9 @@ void LvhEvtVhfGetFeature( } const auto status = copy_vhf_feature_report(*record, *hid_transfer_packet); - trace_status("EvtVhfGetFeature complete", status); + if (!NT_SUCCESS(status)) { + trace_status("EvtVhfGetFeature complete", status); + } static_cast(VhfAsyncOperationComplete(vhf_operation_handle, status)); } diff --git a/src/platform/windows/driver/rotating_trace_log.cpp b/src/platform/windows/driver/rotating_trace_log.cpp new file mode 100644 index 0000000..1e59f33 --- /dev/null +++ b/src/platform/windows/driver/rotating_trace_log.cpp @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: 2026 LIZARDBYTE LLC +// SPDX-License-Identifier: LicenseRef-LizardByte-SAL-1.0 + +/** + * @file src/platform/windows/driver/rotating_trace_log.cpp + * @brief Bounded file logging for the Windows UMDF driver. + */ + +#ifndef NOMINMAX + #define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN +#endif + +// platform includes +#include + +// standard includes +#include +#include +#include +#include + +// local includes +#include "rotating_trace_log.hpp" +#include "unique_win32_handle.hpp" + +namespace lvh::detail::windows { + namespace { + class TraceLogMutex { + public: + [[nodiscard]] std::unique_lock acquire() const { + return std::unique_lock {mutex}; + } + + private: + mutable std::mutex mutex; + }; + + const TraceLogMutex trace_log_mutex; + + bool remove_file_if_present(const std::wstring &path) { + if (DeleteFileW(path.c_str()) != FALSE) { + return true; + } + + const auto error = GetLastError(); + return trace_log_path_missing(error); + } + + bool move_file_if_present(const std::wstring &source, const std::wstring &destination) { + if (MoveFileExW(source.c_str(), destination.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != FALSE) { + return true; + } + + const auto error = GetLastError(); + return trace_log_path_missing(error); + } + + bool rotate_trace_log(const std::wstring &log_path, std::size_t retained_file_count) { + if (retained_file_count == 0U) { + return remove_file_if_present(log_path); + } + + if (!remove_file_if_present(rotated_trace_log_path(log_path, retained_file_count))) { + return false; + } + + for (auto generation = retained_file_count; generation > 1U; --generation) { + if (!move_file_if_present( + rotated_trace_log_path(log_path, generation - 1U), + rotated_trace_log_path(log_path, generation) + )) { + return false; + } + } + + return move_file_if_present(log_path, rotated_trace_log_path(log_path, 1U)); + } + + UniqueWin32Handle open_trace_log(const std::wstring &log_path) { + return make_unique_win32_handle(CreateFileW( + log_path.c_str(), + FILE_APPEND_DATA | FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, + OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr + )); + } + + bool should_rotate(std::uint64_t current_size, std::size_t incoming_size, std::uint64_t max_size) { + if (current_size == 0U) { + return false; + } + if (current_size >= max_size) { + return true; + } + return incoming_size > max_size - current_size; + } + } // namespace + + bool get_trace_log_file_size(HANDLE file, std::uint64_t &size, const TraceLogOperations &operations) { + LARGE_INTEGER file_size {}; + if (operations.get_file_size(file, &file_size) == FALSE) { + return false; + } + size = std::bit_cast(file_size); + return true; + } + + bool write_trace_log_line(HANDLE file, std::string_view line, const TraceLogOperations &operations) { + auto bytes_written = DWORD {}; + const auto bytes_to_write = static_cast(line.size()); + return operations.write_file(file, line.data(), bytes_to_write, &bytes_written, nullptr) != FALSE && + bytes_written == bytes_to_write; + } + + std::wstring rotated_trace_log_path(std::wstring_view log_path, std::size_t generation) { + auto rotated_path = std::wstring {log_path}; + rotated_path.push_back(L'.'); + rotated_path.append(std::to_wstring(generation)); + return rotated_path; + } + + bool append_rotating_trace_log( + std::wstring_view log_path, + std::string_view line, + std::uint64_t max_size, + std::size_t retained_file_count, + const TraceLogOperations &operations + ) { + if (!trace_log_arguments_valid(log_path, line.size(), max_size)) { + return false; + } + + auto lock = trace_log_mutex.acquire(); + + const auto path = std::wstring {log_path}; + auto file = open_trace_log(path); + if (!file) { + return false; + } + + auto file_size = std::uint64_t {}; + if (!get_trace_log_file_size(file.get(), file_size, operations)) { + return false; + } + + if (should_rotate(file_size, line.size(), max_size)) { + file.reset(); + if (!rotate_trace_log(path, retained_file_count)) { + return false; + } + file = open_trace_log(path); + } + + return write_trace_log_line(file.get(), line, operations); + } +} // namespace lvh::detail::windows diff --git a/src/platform/windows/driver/rotating_trace_log.hpp b/src/platform/windows/driver/rotating_trace_log.hpp new file mode 100644 index 0000000..f6946c2 --- /dev/null +++ b/src/platform/windows/driver/rotating_trace_log.hpp @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: 2026 LIZARDBYTE LLC +// SPDX-License-Identifier: LicenseRef-LizardByte-SAL-1.0 + +/** + * @file src/platform/windows/driver/rotating_trace_log.hpp + * @brief Bounded file logging for the Windows UMDF driver. + */ +#pragma once + +#ifndef NOMINMAX + #define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN +#endif + +// platform includes +#include + +// standard includes +#include +#include +#include +#include +#include + +namespace lvh::detail::windows { + /** Maximum size of the active UMDF trace file before it is rotated. */ + inline constexpr std::uint64_t umdf_trace_log_max_size {5ULL * 1024ULL * 1024ULL}; + + /** Number of previous UMDF trace files retained. */ + inline constexpr std::size_t umdf_trace_log_retained_file_count {5}; + + /** Win32 function used to query a trace-log file size. */ + using TraceLogGetFileSize = decltype(&::GetFileSizeEx); + + /** Win32 function used to write a trace-log line. */ + using TraceLogWriteFile = decltype(&::WriteFile); + + /** Win32 operations used by the trace-log writer. */ + struct TraceLogOperations { + TraceLogGetFileSize get_file_size {&::GetFileSizeEx}; + TraceLogWriteFile write_file {&::WriteFile}; + }; + + /** + * @brief Determine whether a trace-log line can be passed to WriteFile. + * @param line_size Line size in bytes. + * @return `true` when the size fits in WriteFile's DWORD parameter. + */ + constexpr bool trace_log_line_size_supported(std::size_t line_size) { + return line_size <= (std::numeric_limits::max)(); + } + + /** + * @brief Determine whether a Win32 path operation failed because the path is absent. + * @param error Win32 error code returned by GetLastError. + * @return `true` for missing files and missing parent paths. + */ + constexpr bool trace_log_path_missing(DWORD error) { + return error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND; + } + + /** + * @brief Validate trace-log append arguments without accessing the line buffer. + * @param log_path Path to the active trace log. + * @param line_size Line size in bytes. + * @param max_size Maximum active-file size in bytes. + * @return `true` when the append arguments are valid. + */ + constexpr bool trace_log_arguments_valid( + std::wstring_view log_path, + std::size_t line_size, + std::uint64_t max_size + ) { + return !log_path.empty() && line_size != 0U && max_size != 0U && trace_log_line_size_supported(line_size); + } + + /** + * @brief Query a trace-log file size without accessing a LARGE_INTEGER union member. + * @param file Open file handle. + * @param size Receives the file size in bytes. + * @param operations Win32 operations used by the trace-log writer. + * @return `true` when the size was retrieved; otherwise `false`. + */ + bool get_trace_log_file_size( + HANDLE file, + std::uint64_t &size, + const TraceLogOperations &operations = {} + ); + + /** + * @brief Write one complete trace-log line. + * @param file Open file handle. + * @param line Complete line to write. + * @param operations Win32 operations used by the trace-log writer. + * @return `true` when every byte was written; otherwise `false`. + */ + bool write_trace_log_line( + HANDLE file, + std::string_view line, + const TraceLogOperations &operations = {} + ); + + /** + * @brief Return the path for a rotated trace-log generation. + * @param log_path Path to the active trace log. + * @param generation Generation number, where one is the newest backup. + * @return The path with the generation number appended. + */ + std::wstring rotated_trace_log_path(std::wstring_view log_path, std::size_t generation); + + /** + * @brief Append a line while keeping the trace log within a bounded set of files. + * + * The active file is rotated before an append would exceed @p max_size. The + * active file becomes `.1`, older generations advance by one, and the oldest + * generation is removed. A single line larger than the limit is written to + * an empty active file so that the diagnostic entry is not silently split. + * + * @param log_path Path to the active trace log. + * @param line Complete line to append. + * @param max_size Maximum active-file size in bytes. + * @param retained_file_count Number of rotated files to retain. + * @param operations Win32 operations used by the trace-log writer. + * @return `true` when the complete line was written; otherwise `false`. + */ + bool append_rotating_trace_log( + std::wstring_view log_path, + std::string_view line, + std::uint64_t max_size = umdf_trace_log_max_size, + std::size_t retained_file_count = umdf_trace_log_retained_file_count, + const TraceLogOperations &operations = {} + ); +} // namespace lvh::detail::windows diff --git a/src/platform/windows/driver/unique_win32_handle.hpp b/src/platform/windows/driver/unique_win32_handle.hpp new file mode 100644 index 0000000..d775b55 --- /dev/null +++ b/src/platform/windows/driver/unique_win32_handle.hpp @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2026 LIZARDBYTE LLC +// SPDX-License-Identifier: LicenseRef-LizardByte-SAL-1.0 + +/** + * @file src/platform/windows/driver/unique_win32_handle.hpp + * @brief RAII ownership for Win32 handles used by the UMDF driver. + */ +#pragma once + +#ifndef NOMINMAX + #define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN +#endif + +// platform includes +#include + +// standard includes +#include + +namespace lvh::detail::windows { + /** + * @brief Unique ownership for a Win32 handle. + */ + using UniqueWin32Handle = std::unique_ptr; + + /** + * @brief Adopt a Win32 handle and normalize invalid handles to null. + * @param handle Handle to adopt. + * @return Unique ownership of a valid handle, or an empty owner. + */ + inline UniqueWin32Handle make_unique_win32_handle(HANDLE handle = nullptr) { + return { + handle == INVALID_HANDLE_VALUE ? nullptr : handle, + &::CloseHandle, + }; + } +} // namespace lvh::detail::windows diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5abeb16..4bc4a83 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -53,10 +53,12 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux") endif() elseif(WIN32) list(APPEND LIBVIRTUALHID_TEST_SOURCES + "${PROJECT_SOURCE_DIR}/src/platform/windows/driver/rotating_trace_log.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/fixtures/windows_backend_test_hooks.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_backend.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_consumers.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_driver_protocol.cpp") + "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_driver_protocol.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_rotating_trace_log.cpp") elseif(APPLE) list(APPEND LIBVIRTUALHID_TEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/fixtures/macos_backend_test_hooks.cpp" @@ -98,6 +100,9 @@ elseif(APPLE) "-framework CoreFoundation" "-framework IOKit") elseif(WIN32) + target_compile_definitions(${TEST_BINARY} + PRIVATE + LIBVIRTUALHID_TEST_BINARY_DIR="${CMAKE_CURRENT_BINARY_DIR}") target_link_libraries(${TEST_BINARY} PRIVATE dinput8 diff --git a/tests/unit/test_windows_consumers.cpp b/tests/unit/test_windows_consumers.cpp index cc9aa77..368d75e 100644 --- a/tests/unit/test_windows_consumers.cpp +++ b/tests/unit/test_windows_consumers.cpp @@ -638,6 +638,7 @@ TEST_F(WindowsConsumerTest, NativeSwitchHandshakeAndInputReportReachHidClient) { EXPECT_EQ(input->at(9), 0x00U); EXPECT_EQ(input->at(10), 0x08U); EXPECT_EQ(input->at(11), 0x00U); + ASSERT_TRUE(created.adapter->close().ok()); } TEST_F(WindowsConsumerTest, NativeXboxPidRumbleWritesAreNormalized) { diff --git a/tests/unit/test_windows_rotating_trace_log.cpp b/tests/unit/test_windows_rotating_trace_log.cpp new file mode 100644 index 0000000..a8e4275 --- /dev/null +++ b/tests/unit/test_windows_rotating_trace_log.cpp @@ -0,0 +1,198 @@ +/** + * @file tests/unit/test_windows_rotating_trace_log.cpp + * @brief Unit tests for the bounded Windows UMDF trace log. + */ + +// platform includes +#include + +// standard includes +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// local includes +#include "fixtures/fixtures.hpp" +#include "platform/windows/driver/rotating_trace_log.hpp" +#include "platform/windows/driver/unique_win32_handle.hpp" + +namespace { + BOOL WINAPI fail_file_size(HANDLE, PLARGE_INTEGER) { + return FALSE; + } + + void write_file(const std::filesystem::path &path, std::string_view content) { + auto output = std::ofstream {path, std::ios::binary}; + output << content; + } + + std::string read_file(const std::filesystem::path &path) { + auto input = std::ifstream {path, std::ios::binary}; + return {std::istreambuf_iterator {input}, std::istreambuf_iterator {}}; + } + + class WindowsRotatingTraceLogTest: public WindowsTest { + protected: + void SetUp() override { + WindowsTest::SetUp(); + std::filesystem::remove_all(test_directory); + std::filesystem::create_directories(test_directory); + } + + void TearDown() override { + std::filesystem::remove_all(test_directory); + WindowsTest::TearDown(); + } + + std::filesystem::path rotated_path(std::size_t generation) const { + return lvh::detail::windows::rotated_trace_log_path(log_path.native(), generation); + } + + const std::filesystem::path test_directory { + std::filesystem::path {LIBVIRTUALHID_TEST_BINARY_DIR} / + std::format("libvirtualhid-rotating-trace-log-tests-{}", GetCurrentProcessId()) + }; + const std::filesystem::path log_path {test_directory / "driver.log"}; + }; + + TEST_F(WindowsRotatingTraceLogTest, AppendsWithoutRotatingAtTheSizeLimit) { + EXPECT_TRUE(lvh::detail::windows::append_rotating_trace_log(log_path.native(), "1234", 8U, 2U)); + EXPECT_TRUE(lvh::detail::windows::append_rotating_trace_log(log_path.native(), "5678", 8U, 2U)); + + EXPECT_EQ(read_file(log_path), "12345678"); + EXPECT_FALSE(std::filesystem::exists(rotated_path(1U))); + } + + TEST_F(WindowsRotatingTraceLogTest, RejectsInvalidArguments) { + EXPECT_FALSE(lvh::detail::windows::append_rotating_trace_log({}, "line", 8U, 2U)); + EXPECT_FALSE(lvh::detail::windows::append_rotating_trace_log(log_path.native(), {}, 8U, 2U)); + EXPECT_FALSE(lvh::detail::windows::append_rotating_trace_log(log_path.native(), "line", 0U, 2U)); + + EXPECT_TRUE( + lvh::detail::windows::trace_log_arguments_valid(log_path.native(), (std::numeric_limits::max)(), 8U) + ); + EXPECT_FALSE( + lvh::detail::windows::trace_log_arguments_valid( + log_path.native(), + static_cast((std::numeric_limits::max)()) + 1U, + 8U + ) + ); + } + + TEST_F(WindowsRotatingTraceLogTest, RejectsAnUnavailableLogPath) { + const auto unavailable_path = test_directory / "missing" / "driver.log"; + EXPECT_FALSE(lvh::detail::windows::append_rotating_trace_log(unavailable_path.native(), "line", 8U, 2U)); + } + + TEST_F(WindowsRotatingTraceLogTest, ReportsFileSizeAndWriteFailures) { + auto operations = lvh::detail::windows::TraceLogOperations {}; + operations.get_file_size = &fail_file_size; + EXPECT_FALSE(lvh::detail::windows::append_rotating_trace_log(log_path.native(), "line", 8U, 2U, operations)); + + operations = {}; + operations.write_file = [](HANDLE, auto, DWORD, LPDWORD bytes_written, LPOVERLAPPED) { + *bytes_written = 0U; + return FALSE; + }; + EXPECT_FALSE(lvh::detail::windows::append_rotating_trace_log(log_path.native(), "line", 8U, 2U, operations)); + + operations.write_file = [](HANDLE, auto, DWORD bytes_to_write, LPDWORD bytes_written, LPOVERLAPPED) { + *bytes_written = bytes_to_write - 1U; + return TRUE; + }; + EXPECT_FALSE(lvh::detail::windows::append_rotating_trace_log(log_path.native(), "line", 8U, 2U, operations)); + } + + TEST_F(WindowsRotatingTraceLogTest, RecognizesMissingPathErrors) { + EXPECT_TRUE(lvh::detail::windows::trace_log_path_missing(ERROR_FILE_NOT_FOUND)); + EXPECT_TRUE(lvh::detail::windows::trace_log_path_missing(ERROR_PATH_NOT_FOUND)); + EXPECT_FALSE(lvh::detail::windows::trace_log_path_missing(ERROR_ACCESS_DENIED)); + } + + TEST_F(WindowsRotatingTraceLogTest, NormalizesAnInvalidWin32Handle) { + EXPECT_FALSE(lvh::detail::windows::make_unique_win32_handle(INVALID_HANDLE_VALUE)); + } + + TEST_F(WindowsRotatingTraceLogTest, RotatesBeforeAnAppendExceedsTheLimit) { + write_file(log_path, "12345678"); + + EXPECT_TRUE(lvh::detail::windows::append_rotating_trace_log(log_path.native(), "new", 8U, 2U)); + + EXPECT_EQ(read_file(log_path), "new"); + EXPECT_EQ(read_file(rotated_path(1U)), "12345678"); + } + + TEST_F(WindowsRotatingTraceLogTest, RetainsOnlyTheConfiguredGenerations) { + constexpr auto max_size = std::uint64_t {4}; + constexpr auto retained_files = std::size_t {2}; + + EXPECT_TRUE(lvh::detail::windows::append_rotating_trace_log(log_path.native(), "aaaa", max_size, retained_files)); + EXPECT_TRUE(lvh::detail::windows::append_rotating_trace_log(log_path.native(), "bbbb", max_size, retained_files)); + EXPECT_TRUE(lvh::detail::windows::append_rotating_trace_log(log_path.native(), "cccc", max_size, retained_files)); + EXPECT_TRUE(lvh::detail::windows::append_rotating_trace_log(log_path.native(), "dddd", max_size, retained_files)); + + EXPECT_EQ(read_file(log_path), "dddd"); + EXPECT_EQ(read_file(rotated_path(1U)), "cccc"); + EXPECT_EQ(read_file(rotated_path(2U)), "bbbb"); + EXPECT_FALSE(std::filesystem::exists(rotated_path(3U))); + } + + TEST_F(WindowsRotatingTraceLogTest, DiscardsTheCurrentLogWhenNoBackupsAreRetained) { + write_file(log_path, "full"); + + EXPECT_TRUE(lvh::detail::windows::append_rotating_trace_log(log_path.native(), "new", 4U, 0U)); + + EXPECT_EQ(read_file(log_path), "new"); + EXPECT_FALSE(std::filesystem::exists(rotated_path(1U))); + } + + TEST_F(WindowsRotatingTraceLogTest, SupportsMissingGenerations) { + write_file(log_path, "current"); + write_file(rotated_path(2U), "second"); + + EXPECT_TRUE(lvh::detail::windows::append_rotating_trace_log(log_path.native(), "new", 7U, 3U)); + + EXPECT_EQ(read_file(log_path), "new"); + EXPECT_EQ(read_file(rotated_path(1U)), "current"); + EXPECT_FALSE(std::filesystem::exists(rotated_path(2U))); + EXPECT_EQ(read_file(rotated_path(3U)), "second"); + } + + TEST_F(WindowsRotatingTraceLogTest, LeavesTheBoundedLogUntouchedWhenRotationFails) { + write_file(log_path, "full"); + std::filesystem::create_directory(rotated_path(1U)); + + EXPECT_FALSE(lvh::detail::windows::append_rotating_trace_log(log_path.native(), "new", 4U, 1U)); + + EXPECT_EQ(read_file(log_path), "full"); + EXPECT_TRUE(std::filesystem::is_directory(rotated_path(1U))); + } + + TEST_F(WindowsRotatingTraceLogTest, ReportsALockedGenerationMoveFailure) { + write_file(log_path, "full"); + write_file(rotated_path(1U), "previous"); + const auto locked_generation = lvh::detail::windows::make_unique_win32_handle(CreateFileW( + rotated_path(1U).c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + nullptr + )); + ASSERT_TRUE(locked_generation); + + EXPECT_FALSE(lvh::detail::windows::append_rotating_trace_log(log_path.native(), "new", 4U, 2U)); + + EXPECT_EQ(read_file(log_path), "full"); + EXPECT_EQ(read_file(rotated_path(1U)), "previous"); + EXPECT_FALSE(std::filesystem::exists(rotated_path(2U))); + } +} // namespace