Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions docs/windows-driver.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion src/platform/windows/driver/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 16 additions & 70 deletions src/platform/windows/driver/libvirtualhid_umdf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
#include <cstdint>
#include <cstring>
#include <format>
#include <limits>
#include <map>
#include <memory>
#include <mutex>
Expand All @@ -48,14 +47,15 @@
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>

// 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.
Expand Down Expand Up @@ -84,47 +84,6 @@ namespace {
std::remove_pointer_t<SC_HANDLE>,
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<void>(CloseHandle(handle_));
}
handle_ = handle;
}

private:
HANDLE handle_ {};
};

struct DeviceRecord {
std::mutex mutex;
std::uint64_t driver_device_id {};
Expand Down Expand Up @@ -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);

Expand All @@ -212,12 +158,7 @@ namespace {
step,
static_cast<unsigned long>(status)
);
DWORD bytes_written {};
const auto bytes_to_write =
static_cast<DWORD>(std::min(line.size(), static_cast<std::size_t>(std::numeric_limits<DWORD>::max())));
static_cast<void>(WriteFile(file, line.data(), bytes_to_write, &bytes_written, nullptr));

static_cast<void>(CloseHandle(file));
static_cast<void>(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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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<void>(VhfAsyncOperationComplete(vhf_operation_handle, status));
}

Expand Down
162 changes: 162 additions & 0 deletions src/platform/windows/driver/rotating_trace_log.cpp
Original file line number Diff line number Diff line change
@@ -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 <Windows.h>

// standard includes
#include <bit>
#include <mutex>
#include <string>
#include <string_view>

// local includes
#include "rotating_trace_log.hpp"
#include "unique_win32_handle.hpp"

namespace lvh::detail::windows {
namespace {
class TraceLogMutex {
public:
[[nodiscard]] std::unique_lock<std::mutex> 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<std::uint64_t>(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<DWORD>(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
Loading
Loading