diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5a76c83..102d367 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: shell: cmd run: | cl /EHsc /std:c++20 /permissive- /I. /DUNICODE /D_UNICODE /GS /sdl ^ - cli_args_debugger.cpp seh_wrapper.cpp qrcodegen.cpp ^ + cli_args_debugger.cpp log_manager.cpp seh_wrapper.cpp qrcodegen.cpp ^ /Fe:build\cloud-streaming-args-debugger.exe ^ /Fo:obj\ ^ /link d3d11.lib d3dcompiler.lib d2d1.lib dwrite.lib ole32.lib avrt.lib user32.lib shell32.lib gdi32.lib propsys.lib winmm.lib psapi.lib diff --git a/.gitignore b/.gitignore index 6291ef3..b66fcd3 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,7 @@ cmake_install.cmake # QR Code generator source files (downloaded during build) qrcodegen.cpp -qrcodegen.hpp \ No newline at end of file +qrcodegen.hpp + +# Local static-analysis artifacts +/.analysis/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 6273931..c2f6b4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_CURRENT_SOURCE_DIR}/build) add_executable(cloud-streaming-args-debugger WIN32 # Specify that the application uses WinMain instead of main cli_args_debugger.cpp + log_manager.cpp seh_wrapper.cpp qrcodegen.cpp # Include QR code generator ) diff --git a/README.md b/README.md index f8dc459..203aea5 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Check out a preview of the application: # Compile with MSVC cl /EHsc /std:c++20 /permissive- /I. /DUNICODE /D_UNICODE ^ - cli_args_debugger.cpp seh_wrapper.cpp qrcodegen.cpp ^ + cli_args_debugger.cpp log_manager.cpp seh_wrapper.cpp qrcodegen.cpp ^ /Fe:build/ArgumentDebugger.exe ^ /Fo:build/ ^ /link d3d11.lib d3dcompiler.lib d2d1.lib dwrite.lib ole32.lib avrt.lib user32.lib shell32.lib gdi32.lib propsys.lib diff --git a/cli_args_debugger.cpp b/cli_args_debugger.cpp index dfbe955..f4a15a5 100644 --- a/cli_args_debugger.cpp +++ b/cli_args_debugger.cpp @@ -89,6 +89,10 @@ #include "qrcodegen.hpp" using qrcodegen::QrCode; +// Logger lives in log_manager.{hpp,cpp} so multiple translation units (tests, +// seh_wrapper) can link against the same instance. +#include "log_manager.hpp" + // Use Microsoft::WRL::ComPtr for COM object management using Microsoft::WRL::ComPtr; @@ -101,129 +105,6 @@ using Microsoft::WRL::ComPtr; throw std::runtime_error(msg); \ } while (0) -// =========================================================================== -// LOGGING SUPPORT -// =========================================================================== - -// Simple header-only logger that writes UTF-16 text to a file -// located next to the executable (CloudStreamingArgsDebugger.log). -// -// • InitLogger() – call once from wWinMain right after COM is up. -// • Log(L"text") – append one line with local‑time prefix. -// • ShowLogs() – read the file and place it into loaded_data_ so it is -// rendered in the right‑upper corner (triggered via "logs") - -// Global variables for logging (exported for tests) -FILE* g_log_file = nullptr; // File handle for the log file -std::wstring g_logPath; // Path to the log file - -namespace // anonymous, internal -{ -CRITICAL_SECTION g_log_cs; // Critical section for thread safety -} // namespace - -void InitLogger() -{ - PWSTR appdata_path = nullptr; - HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &appdata_path); - if (SUCCEEDED(hr)) - { - g_logPath.assign(appdata_path); - CoTaskMemFree(appdata_path); - g_logPath += L"\\CloudStreamingArgsDebugger\\debug.log"; - std::wstring dir = g_logPath.substr(0, g_logPath.find_last_of(L"\\")); - CreateDirectoryW(dir.c_str(), nullptr); - } - else - { - wchar_t exePath[MAX_PATH] = L"\0"; - GetModuleFileNameW(nullptr, exePath, MAX_PATH); - g_logPath.assign(exePath); - size_t pos = g_logPath.find_last_of(L"\\/"); - if (pos != std::wstring::npos) - g_logPath.erase(pos + 1); - g_logPath += L"CloudStreamingArgsDebugger.log"; - } - - // Open file in append mode with UTF-16LE encoding - // Use _SH_DENYNO to allow other handles to read the file while we have it open - g_log_file = _wfsopen(g_logPath.c_str(), L"a+, ccs=UTF-16LE", _SH_DENYNO); - - // If this is a new file, write BOM (Byte Order Mark) - if (g_log_file && ftell(g_log_file) == 0) - { - fputwc(0xFEFF, g_log_file); // UTF-16LE BOM - } - - // Initialize critical section for thread-safe logging - InitializeCriticalSection(&g_log_cs); -} - -void Log(const std::wstring& text) -{ - if (!g_log_file) - return; - - // Lock the critical section before accessing the file - EnterCriticalSection(&g_log_cs); - - SYSTEMTIME st; - GetLocalTime(&st); - - // Format log entry with timestamp - std::wstring entry = L"["; - entry += std::to_wstring(st.wYear) + L"-"; - - // Month with padding - if (st.wMonth < 10) - entry += L"0"; - entry += std::to_wstring(st.wMonth) + L"-"; - - // Day with padding - if (st.wDay < 10) - entry += L"0"; - entry += std::to_wstring(st.wDay) + L" "; - - // Hour with padding - if (st.wHour < 10) - entry += L"0"; - entry += std::to_wstring(st.wHour) + L":"; - - // Minute with padding - if (st.wMinute < 10) - entry += L"0"; - entry += std::to_wstring(st.wMinute) + L":"; - - // Second with padding - if (st.wSecond < 10) - entry += L"0"; - entry += std::to_wstring(st.wSecond) + L"] "; - - // Add the actual log message - entry += text + L"\n"; - - // Write to file and flush - fputws(entry.c_str(), g_log_file); - fflush(g_log_file); - - // Unlock the critical section - LeaveCriticalSection(&g_log_cs); -} - -// Simple log function for SEH wrapper to use -// Exported for seh_wrapper.cpp -void LogSEH(const wchar_t* message) -{ - if (message) - { - // Convert C-string to wstring for main Log function - Log(std::wstring(message)); - // Also output to debug console for immediate visibility - OutputDebugStringW(message); - OutputDebugStringW(L"\n"); - } -} - // Helper function: robust conversion from std::wstring to std::string using WideCharToMultiByte (UTF-8) std::string wstring_to_string(const std::wstring& wstr) { @@ -450,35 +331,17 @@ int WINAPI wWinMain(HINSTANCE h_instance, HINSTANCE, PWSTR, int cmd_show) Log(L"Application exit, code = " + std::to_wstring(exit_code)); Log(L"wWinMain: leaving, exitCode=" + std::to_wstring(exit_code)); - // Close the log file and delete critical section - if (g_log_file) - { - fclose(g_log_file); - g_log_file = nullptr; - } - - // Delete critical section - DeleteCriticalSection(&g_log_cs); - + CloseLogger(); CoUninitialize(); return exit_code; } catch (const std::exception& ex) { - // Convert char* to wstring for logging std::wstring wstr(ex.what(), ex.what() + strlen(ex.what())); Log(L"Unhandled C++ exception"); Log(L"FATAL: " + wstr); - // Close log file before exit - if (g_log_file) - { - fclose(g_log_file); - g_log_file = nullptr; - } - - // Delete critical section in exception case as well - DeleteCriticalSection(&g_log_cs); + CloseLogger(); MessageBoxA(nullptr, ex.what(), "Initialization Error", MB_OK | MB_ICONERROR); return -1; diff --git a/log_manager.cpp b/log_manager.cpp new file mode 100644 index 0000000..32b4fff --- /dev/null +++ b/log_manager.cpp @@ -0,0 +1,115 @@ +#ifndef UNICODE +#define UNICODE +#define _UNICODE +#endif + +#include "log_manager.hpp" + +#include +#include +#include + +#pragma comment(lib, "shell32") +#pragma comment(lib, "ole32") + +FILE* g_log_file = nullptr; +std::wstring g_logPath; + +namespace +{ +CRITICAL_SECTION g_log_cs; +bool g_log_cs_initialized = false; +} // namespace + +void InitLogger() +{ + PWSTR appdata_path = nullptr; + HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &appdata_path); + if (SUCCEEDED(hr)) + { + g_logPath.assign(appdata_path); + CoTaskMemFree(appdata_path); + g_logPath += L"\\CloudStreamingArgsDebugger\\debug.log"; + std::wstring dir = g_logPath.substr(0, g_logPath.find_last_of(L"\\")); + CreateDirectoryW(dir.c_str(), nullptr); + } + else + { + wchar_t exePath[MAX_PATH] = L"\0"; + GetModuleFileNameW(nullptr, exePath, MAX_PATH); + g_logPath.assign(exePath); + size_t pos = g_logPath.find_last_of(L"\\/"); + if (pos != std::wstring::npos) + g_logPath.erase(pos + 1); + g_logPath += L"CloudStreamingArgsDebugger.log"; + } + + g_log_file = _wfsopen(g_logPath.c_str(), L"a+, ccs=UTF-16LE", _SH_DENYNO); + + if (g_log_file && ftell(g_log_file) == 0) + { + fputwc(0xFEFF, g_log_file); // UTF-16LE BOM + } + + InitializeCriticalSection(&g_log_cs); + g_log_cs_initialized = true; +} + +void Log(const std::wstring& text) +{ + if (!g_log_file || !g_log_cs_initialized) + return; + + EnterCriticalSection(&g_log_cs); + + SYSTEMTIME st; + GetLocalTime(&st); + + std::wstring entry = L"["; + entry += std::to_wstring(st.wYear) + L"-"; + if (st.wMonth < 10) + entry += L"0"; + entry += std::to_wstring(st.wMonth) + L"-"; + if (st.wDay < 10) + entry += L"0"; + entry += std::to_wstring(st.wDay) + L" "; + if (st.wHour < 10) + entry += L"0"; + entry += std::to_wstring(st.wHour) + L":"; + if (st.wMinute < 10) + entry += L"0"; + entry += std::to_wstring(st.wMinute) + L":"; + if (st.wSecond < 10) + entry += L"0"; + entry += std::to_wstring(st.wSecond) + L"] "; + + entry += text + L"\n"; + + fputws(entry.c_str(), g_log_file); + fflush(g_log_file); + + LeaveCriticalSection(&g_log_cs); +} + +void LogSEH(const wchar_t* message) +{ + if (!message) + return; + Log(std::wstring(message)); + OutputDebugStringW(message); + OutputDebugStringW(L"\n"); +} + +void CloseLogger() +{ + if (g_log_file) + { + fclose(g_log_file); + g_log_file = nullptr; + } + if (g_log_cs_initialized) + { + DeleteCriticalSection(&g_log_cs); + g_log_cs_initialized = false; + } +} diff --git a/log_manager.hpp b/log_manager.hpp new file mode 100644 index 0000000..4f931c5 --- /dev/null +++ b/log_manager.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +// UTF-16LE log file written next to the executable (or to %APPDATA%\CloudStreamingArgsDebugger\). +// +// InitLogger() - call once from wWinMain after COM is initialized. +// Log(L"...") - append one line prefixed with local time, thread-safe. +// LogSEH(...) - thin wrapper used from the SEH-guarded audio thread. +// CloseLogger() - flush, close the file, and release the critical section. +// GetLogPath() - path to the log file (empty before InitLogger()). +// +// The raw globals `g_log_file` and `g_logPath` remain visible with extern +// linkage so that existing unit tests (tests/logging_tests.cpp) keep compiling. + +extern FILE* g_log_file; +extern std::wstring g_logPath; + +void InitLogger(); +void Log(const std::wstring& text); +void LogSEH(const wchar_t* message); +void CloseLogger(); + +inline const std::wstring& GetLogPath() +{ + return g_logPath; +} +inline FILE* GetLogFile() +{ + return g_log_file; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6656402..72c8de9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -35,6 +35,7 @@ set(TEST_SOURCES set(PARENT_SOURCES ../qrcodegen.cpp ../cli_args_debugger.cpp + ../log_manager.cpp ../seh_wrapper.cpp ) diff --git a/validate.sh b/validate.sh index 2b59ae8..b6dbf3e 100755 --- a/validate.sh +++ b/validate.sh @@ -19,7 +19,7 @@ echo "🔍 Checking for common C++ syntax issues..." # Check for missing semicolons, unmatched braces, etc. syntax_errors=0 -for file in cli_args_debugger.cpp seh_wrapper.cpp; do +for file in cli_args_debugger.cpp seh_wrapper.cpp log_manager.cpp; do if [ -f "$file" ]; then echo "Checking $file..."