From 2c31af1b41ba67124263eb9b4f967e201bd5f742 Mon Sep 17 00:00:00 2001 From: hy Date: Fri, 29 May 2026 15:24:25 +0900 Subject: [PATCH 1/2] feat: include machine name header and optimize --- CMakeLists.txt | 13 ++- include/globals.h | 9 +++ include/process_monitor.h | 6 +- include/wakatime_client.h | 8 +- main.cpp | 64 +++++++-------- src/file_watcher.cpp | 38 ++++----- src/process_monitor.cpp | 24 +++--- src/tray_icon.cpp | 66 ++++++++------- src/wakatime_client.cpp | 166 +++++++++++++++++++++++--------------- 9 files changed, 223 insertions(+), 171 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 00330b8..bbeff7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,9 +82,18 @@ if(WIN32) /ENTRY:mainCRTStartup ) elseif(MINGW) - # MinGW: 심볼 스트리핑 (크기 최적화) + # MinGW: 크기 최적화 (-Os), LTO, 미사용 섹션 제거 (Resident Set Size 최소화) + target_compile_options(unity_wakatime PRIVATE + -Os + -flto + -ffunction-sections + -fdata-sections + ) target_link_options(unity_wakatime PRIVATE - -s # Strip symbols for smaller file size + -Os # LTO 링크 단계 최적화도 크기 우선으로 + -s # Strip symbols for smaller file size + -flto + -Wl,--gc-sections ) endif() endif() diff --git a/include/globals.h b/include/globals.h index 8a7a6dd..1f379ab 100644 --- a/include/globals.h +++ b/include/globals.h @@ -28,6 +28,15 @@ namespace fs = std::filesystem; +// Release(NDEBUG)에서는 인자 평가까지 컴파일아웃된다. +#ifdef NDEBUG + #define WT_LOG(msg) ((void)0) + #define WT_ERR(msg) ((void)0) +#else + #define WT_LOG(msg) do { std::cout << msg << std::endl; } while (0) + #define WT_ERR(msg) do { std::cerr << msg << std::endl; } while (0) +#endif + class WakaTimeClient; class FileWatcher; class ProcessMonitor; diff --git a/include/process_monitor.h b/include/process_monitor.h index 78d6632..b8b14fa 100644 --- a/include/process_monitor.h +++ b/include/process_monitor.h @@ -2,7 +2,7 @@ #include "globals.h" #include // WMI 인터페이스 -#include +#include #pragma comment(lib, "wbemuuid.lib") #pragma comment(lib, "ole32.lib") @@ -13,7 +13,7 @@ */ class ProcessMonitor { private: - std::map activeInstances; + std::unordered_map activeInstances; IWbemLocator* pLocator = nullptr; IWbemServices* pService = nullptr; bool wmiInitialized = false; @@ -124,5 +124,5 @@ class ProcessMonitor { * 현재 활성화된 모든 Unity 인스턴스 반환 * @return 현재 실행중인 인스턴스들 */ - const std::map& GetActiveInstances() const; + const std::unordered_map& GetActiveInstances() const; }; \ No newline at end of file diff --git a/include/wakatime_client.h b/include/wakatime_client.h index 42fbdce..948b299 100644 --- a/include/wakatime_client.h +++ b/include/wakatime_client.h @@ -3,6 +3,7 @@ #include "globals.h" #include // Windows HTTP API #include +#include #pragma comment(lib, "winhttp.lib") // WinHTTP 라이브러리 링크 @@ -17,7 +18,6 @@ struct HeartbeatData { std::string language; // Unity std::string editor; // "Unity" std::string operating_system; // "Windows" - std::string machine; // 머신 이름 int64_t time; // Unix timestamp bool is_write; // 파일 수정 여부 @@ -40,10 +40,12 @@ class WakaTimeClient { // HTTP 세션 관리 HINTERNET hSession; // WinHTTP 세션 핸들 + HINTERNET hConnect; // WinHTTP 연결 핸들 (heartbeat 간 재사용, keep-alive) bool initialized; // 초기화 상태 - + // 비동기 전송 관리 mutable std::mutex queueMutex; // 큐 접근 동기화 + std::condition_variable queueCv; // 큐 대기/통지 (busy-poll 제거) std::queue heartbeatQueue; // 전송 대기 큐 std::chrono::steady_clock::time_point lastQueuedAt; std::string lastQueuedEntity; @@ -92,7 +94,7 @@ class WakaTimeClient { * @return 머신 이름 */ std::string GetMachineName(); - + /** * Unix timestamp 생성 (현재 시간) * @return Unix timestamp (초 단위) diff --git a/main.cpp b/main.cpp index ef474cd..e55e970 100644 --- a/main.cpp +++ b/main.cpp @@ -29,7 +29,7 @@ namespace Globals // 파일 변경 이벤트 처리 - TrayIcon도 업데이트 void OnFileChanged(const FileChangeEvent &event) { - std::cout << "[HEARTBEAT] " << event.fileName << " (" << event.projectName << ")" << std::endl; + WT_LOG("[HEARTBEAT] " << event.fileName << " (" << event.projectName << ")"); // WakaTime API에 heartbeat 전송 if (g_wakatimeClient) @@ -37,11 +37,11 @@ void OnFileChanged(const FileChangeEvent &event) if (g_wakatimeClient->IsInitialized()) { g_wakatimeClient->SendHeartbeatFromEvent(event); - std::cout << "[HEARTBEAT] ✅ Sent to WakaTime API" << std::endl; + WT_LOG("[HEARTBEAT] ✅ Sent to WakaTime API"); } else { - std::cout << "[HEARTBEAT] ⚠️ Skipped - WakaTime client not initialized" << std::endl; + WT_LOG("[HEARTBEAT] ⚠️ Skipped - WakaTime client not initialized"); } } @@ -56,7 +56,7 @@ void OnFileChanged(const FileChangeEvent &event) // 트레이 아이콘 콜백 함수들 void OnTrayExit() { - std::cout << "[Main] Exit requested from tray" << std::endl; + WT_LOG("[Main] Exit requested from tray"); Globals::RequestExit(); } @@ -71,7 +71,7 @@ void OnTrayShowStatus() void OnTrayToggleMonitoring(const bool enabled) { - std::cout << "[Main] Monitoring " << (enabled ? "enabled" : "disabled") << std::endl; + WT_LOG("[Main] Monitoring " << (enabled ? "enabled" : "disabled")); if (g_trayIcon) { @@ -84,7 +84,7 @@ void OnTrayToggleMonitoring(const bool enabled) void OnTrayOpenDashboard() { - std::cout << "[Main] Opening WakaTime dashboard" << std::endl; + WT_LOG("[Main] Opening WakaTime dashboard"); ShellExecuteW(nullptr, L"open", L"https://wakatime.com/dashboard", nullptr, nullptr, SW_SHOWNORMAL); @@ -92,7 +92,7 @@ void OnTrayOpenDashboard() void OnTrayShowSettings() { - std::cout << "[Main] Settings requested" << std::endl; + WT_LOG("[Main] Settings requested"); if (g_trayIcon && g_wakatimeClient) { @@ -106,7 +106,7 @@ void OnTrayShowSettings() void OnApiKeyChanged(const std::string &newApiKey) { - std::cout << "[Main] API Key changed, updating WakaTime client..." << std::endl; + WT_LOG("[Main] API Key changed, updating WakaTime client..."); if (g_wakatimeClient) { @@ -118,12 +118,12 @@ void OnApiKeyChanged(const std::string &newApiKey) { g_trayIcon->ShowInfoNotification("✅ API Key saved and client reinitialized!"); g_trayIcon->RefreshStatusMenu(); - std::cout << "[Main] ✅ WakaTime client successfully reinitialized" << std::endl; + WT_LOG("[Main] ✅ WakaTime client successfully reinitialized"); } else { g_trayIcon->ShowErrorNotification("❌ Failed to initialize with new API key"); - std::cerr << "[Main] ❌ WakaTime client reinitialization failed" << std::endl; + WT_ERR("[Main] ❌ WakaTime client reinitialization failed"); } } } @@ -156,12 +156,12 @@ void HandleNewUnityInstances(const std::vector &newInstances) { for (const auto &instance: newInstances) { - std::cout << "[Main] New Unity instance detected: " << instance.projectName - << " (Unity " << instance.editorVersion << ")" << std::endl; + WT_LOG("[Main] New Unity instance detected: " << instance.projectName + << " (Unity " << instance.editorVersion << ")"); if (g_fileWatcher && g_fileWatcher->StartWatching(instance.projectPath, instance.projectName, instance.editorVersion)) { - std::cout << "[Main] Started watching: " << instance.projectName << std::endl; + WT_LOG("[Main] Started watching: " << instance.projectName); if (g_trayIcon) { @@ -172,7 +172,7 @@ void HandleNewUnityInstances(const std::vector &newInstances) } else { - std::cout << "[Main] Failed to start watching: " << instance.projectName << std::endl; + WT_LOG("[Main] Failed to start watching: " << instance.projectName); } } } @@ -181,7 +181,7 @@ void HandleClosedUnityInstances(const std::vector &closedInstance { for (const auto &instance: closedInstances) { - std::cout << "[Main] Unity instance closed: " << instance.projectName << std::endl; + WT_LOG("[Main] Unity instance closed: " << instance.projectName); if (g_fileWatcher) { @@ -199,11 +199,11 @@ void HandleClosedUnityInstances(const std::vector &closedInstance { const auto& projectName = remainingProjects[0].projectName; g_trayIcon->SetCurrentProject(projectName); - std::cout << "[Main] Switched to remaining project: " << projectName << std::endl; + WT_LOG("[Main] Switched to remaining project: " << projectName); } else { g_trayIcon->SetCurrentProject(""); // 모든 프로젝트 종료 - std::cout << "[Main] No Unity projects are being watched" << std::endl; + WT_LOG("[Main] No Unity projects are being watched"); } } } @@ -214,13 +214,13 @@ void InitialUnityProjectScan() { if (!g_processMonitor || !g_fileWatcher) return; - std::cout << "[Main] Performing initial Unity project scan..." << std::endl; + WT_LOG("[Main] Performing initial Unity project scan..."); const auto& instances = g_processMonitor->ScanUnityProcesses(); if (instances.empty()) { - std::cout << "[Main] No Unity processes found during initial scan" << std::endl; + WT_LOG("[Main] No Unity processes found during initial scan"); return; } @@ -228,8 +228,8 @@ void InitialUnityProjectScan() { if (g_fileWatcher->StartWatching(instance.projectPath, instance.projectName, instance.editorVersion)) { - std::cout << "[Main] ✅ Started watching: " << instance.projectName - << " (Unity " << instance.editorVersion << ")" << std::endl; + WT_LOG("[Main] ✅ Started watching: " << instance.projectName + << " (Unity " << instance.editorVersion << ")"); if (g_trayIcon) { @@ -238,22 +238,22 @@ void InitialUnityProjectScan() } } - std::cout << "[Main] Initial scan complete. Watching " << instances.size() << " Unity projects" << std::endl; + WT_LOG("[Main] Initial scan complete. Watching " << instances.size() << " Unity projects"); } int main() { - std::cout << "[Main] Unity WakaTime Monitor Starting..." << std::endl; + WT_LOG("[Main] Unity WakaTime Monitor Starting..."); const bool darkModeAvailable = WindowsDarkMode::EnableForApp(); - std::cout << "[Main] Dark mode menu opt-in: " - << (darkModeAvailable ? "enabled" : "not available") << std::endl; + WT_LOG("[Main] Dark mode menu opt-in: " + << (darkModeAvailable ? "enabled" : "not available")); TrayIcon trayIcon; g_trayIcon = &trayIcon; if (!trayIcon.Initialize("Unity WakaTime")) { - std::cerr << "[Main] Failed to initialize tray icon!" << std::endl; + WT_ERR("[Main] Failed to initialize tray icon!"); return 1; } @@ -272,7 +272,7 @@ int main() if (!wakatimeClient.Initialize()) { - std::cerr << "[Main] Failed to initialize WakaTime client!" << std::endl; + WT_ERR("[Main] Failed to initialize WakaTime client!"); trayIcon.ShowErrorNotification("WakaTime client not initialized. Click 'Setup API Key' in menu."); } @@ -294,7 +294,7 @@ int main() trayIcon.SetMonitoringState(true); - std::cout << "\n[Main] Unity WakaTime is now running in background!" << std::endl; + WT_LOG("\n[Main] Unity WakaTime is now running in background!"); auto lastScan = std::chrono::steady_clock::now(); const auto scanInterval = std::chrono::seconds(10); @@ -334,7 +334,7 @@ int main() } } - std::cout << "\n[Main] Shutting down Unity WakaTime..." << std::endl; + WT_LOG("\n[Main] Shutting down Unity WakaTime..."); trayIcon.ShowInfoNotification("Unity WakaTime shutting down..."); // 남은 heartbeat 전송 @@ -345,19 +345,19 @@ int main() g_fileWatcher->DrainPendingEvents(2048); } - std::cout << "[Main] Flushing remaining heartbeats..." << std::endl; + WT_LOG("[Main] Flushing remaining heartbeats..."); wakatimeClient.FlushQueue(); } // 모든 파일 감시 중지 if (g_fileWatcher) { - std::cout << "[Main] Stopping all file watchers..." << std::endl; + WT_LOG("[Main] Stopping all file watchers..."); g_fileWatcher->StopAllWatching(); } Globals::Cleanup(); - std::cout << "[Main] Unity WakaTime stopped gracefully." << std::endl; + WT_LOG("[Main] Unity WakaTime stopped gracefully."); return 0; } diff --git a/src/file_watcher.cpp b/src/file_watcher.cpp index 9ff3c00..5670a3c 100644 --- a/src/file_watcher.cpp +++ b/src/file_watcher.cpp @@ -9,19 +9,19 @@ namespace FileWatcher::FileWatcher() { - std::cout << "[FileWatcher] Initialized" << std::endl; + WT_LOG("[FileWatcher] Initialized"); } FileWatcher::~FileWatcher() { StopAllWatching(); - std::cout << "[FileWatcher] Destroyed" << std::endl; + WT_LOG("[FileWatcher] Destroyed"); } void FileWatcher::SetChangeCallback(std::function callback) { changeCallback = std::move(callback); - std::cout << "[FileWatcher] Change callback set" << std::endl; + WT_LOG("[FileWatcher] Change callback set"); } bool FileWatcher::StartWatching(const std::string &projectPath, const std::string &projectName, const std::string &unityVersion) @@ -37,7 +37,7 @@ bool FileWatcher::StartWatching(const std::string &projectPath, const std::strin // 프로젝트 경로가 존재하는지 확인 if (!fs::exists(projectPath)) { - std::cerr << "[FileWatcher] Project path does not exist: " << projectPath << std::endl; + WT_ERR("[FileWatcher] Project path does not exist: " << projectPath); return false; } @@ -45,7 +45,7 @@ bool FileWatcher::StartWatching(const std::string &projectPath, const std::strin auto project = std::make_unique(); if (project->stopEvent == nullptr || project->ioEvent == nullptr) { - std::cerr << "[FileWatcher] Failed to create watcher events for: " << projectPath << std::endl; + WT_ERR("[FileWatcher] Failed to create watcher events for: " << projectPath); return false; } @@ -77,11 +77,11 @@ bool FileWatcher::StartWatching(const std::string &projectPath, const std::strin if (project->directoryHandle == INVALID_HANDLE_VALUE) { const DWORD error = GetLastError(); - std::cerr << "[FileWatcher] Failed to open directory: " << projectPath << " (Error: " << error << ")" << std::endl; + WT_ERR("[FileWatcher] Failed to open directory: " << projectPath << " (Error: " << error << ")"); return false; } - std::cout << "[FileWatcher] Started watching: " << projectName << " at " << projectPath << std::endl; + WT_LOG("[FileWatcher] Started watching: " << projectName << " at " << projectPath); // 감시 스레드 시작 WatchedProject *projectPtr = project.get(); @@ -91,7 +91,7 @@ bool FileWatcher::StartWatching(const std::string &projectPath, const std::strin } catch (const std::system_error &e) { - std::cerr << "[FileWatcher] Failed to start watch thread for " << projectName << ": " << e.what() << std::endl; + WT_ERR("[FileWatcher] Failed to start watch thread for " << projectName << ": " << e.what()); return false; } @@ -108,11 +108,11 @@ void FileWatcher::WatchProjectThread(WatchedProject *project) project->stopEvent == nullptr || project->ioEvent == nullptr) { - std::cerr << "[FileWatcher] Invalid watch project state, thread exit" << std::endl; + WT_ERR("[FileWatcher] Invalid watch project state, thread exit"); return; } - std::cout << "[FileWatcher] Watch thread started for: " << project->projectName << std::endl; + WT_LOG("[FileWatcher] Watch thread started for: " << project->projectName); while (!project->shouldStop) { @@ -138,7 +138,7 @@ void FileWatcher::WatchProjectThread(WatchedProject *project) { if (const DWORD error = GetLastError(); error != ERROR_IO_PENDING) { - std::cerr << "[FileWatcher] ReadDirectoryChangesW failed for " << project->projectName << " (Error: " << error << ")" << std::endl; + WT_ERR("[FileWatcher] ReadDirectoryChangesW failed for " << project->projectName << " (Error: " << error << ")"); break; } } @@ -179,7 +179,7 @@ void FileWatcher::WatchProjectThread(WatchedProject *project) { if (const DWORD error = GetLastError(); error != ERROR_OPERATION_ABORTED && error != ERROR_IO_INCOMPLETE) { - std::cerr << "[FileWatcher] GetOverlappedResult failed for " << project->projectName << " (Error: " << error << ")" << std::endl; + WT_ERR("[FileWatcher] GetOverlappedResult failed for " << project->projectName << " (Error: " << error << ")"); } if (project->shouldStop) { @@ -190,17 +190,17 @@ void FileWatcher::WatchProjectThread(WatchedProject *project) } case (WAIT_OBJECT_0 + 1): // stopEvent 신호 (종료 요청) - std::cout << "[FileWatcher] Stop event received for: " << project->projectName << std::endl; + WT_LOG("[FileWatcher] Stop event received for: " << project->projectName); goto thread_exit; // 즉시 종료 default: // 에러 - std::cerr << "[FileWatcher] WaitForMultipleObjects failed for " << project->projectName << " (Error: " << GetLastError() << ")" << std::endl; + WT_ERR("[FileWatcher] WaitForMultipleObjects failed for " << project->projectName << " (Error: " << GetLastError() << ")"); goto thread_exit; } } thread_exit: - std::cout << "[FileWatcher] Watch thread stopped for: " << project->projectName << std::endl; + WT_LOG("[FileWatcher] Watch thread stopped for: " << project->projectName); } void FileWatcher::ProcessFileChanges(char *buffer, DWORD bytesReturned, WatchedProject *project) @@ -254,7 +254,7 @@ void FileWatcher::ProcessFileChanges(char *buffer, DWORD bytesReturned, WatchedP event.action = info->Action; event.timestamp = std::chrono::system_clock::now(); - std::cout << "[FileWatcher] Change" << ": " << fileName << " in " << project->projectName << std::endl; + WT_LOG("[FileWatcher] Change" << ": " << fileName << " in " << project->projectName); // 워커 스레드에서 직접 UI/앱 콜백을 호출하지 않고 큐에 적재 { @@ -334,7 +334,7 @@ void FileWatcher::StopWatching(const std::string &projectPath) watchedProjects.erase(it); } - std::cout << "[FileWatcher] Stopping watch for: " << projectToStop->projectName << std::endl; + WT_LOG("[FileWatcher] Stopping watch for: " << projectToStop->projectName); projectToStop->shouldStop = true; if (projectToStop->stopEvent != nullptr) @@ -360,7 +360,7 @@ void FileWatcher::StopAllWatching() projectsToStop.swap(watchedProjects); } - std::cout << "[FileWatcher] Stopping all watches..." << std::endl; + WT_LOG("[FileWatcher] Stopping all watches..."); for (const auto &project: projectsToStop) { @@ -384,7 +384,7 @@ void FileWatcher::StopAllWatching() } } - std::cout << "[FileWatcher] All watches stopped" << std::endl; + WT_LOG("[FileWatcher] All watches stopped"); } void FileWatcher::DrainPendingEvents(const size_t maxEvents) diff --git a/src/process_monitor.cpp b/src/process_monitor.cpp index 7d41db9..5df5252 100644 --- a/src/process_monitor.cpp +++ b/src/process_monitor.cpp @@ -3,14 +3,14 @@ ProcessMonitor::ProcessMonitor() { - std::cout << "[ProcessMonitor] Initialized" << std::endl; + WT_LOG("[ProcessMonitor] Initialized"); } ProcessMonitor::~ProcessMonitor() { CleanupWMI(); activeInstances.clear(); - std::cout << "[ProcessMonitor] Destroyed" << std::endl; + WT_LOG("[ProcessMonitor] Destroyed"); } BSTR ProcessMonitor::StringToBSTR(const std::wstring &str) @@ -36,7 +36,7 @@ bool ProcessMonitor::InitializeWMI() HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); if (FAILED(hr)) { - std::cerr << "[ProcessMonitor] COM initialization failed" << std::endl; + WT_ERR("[ProcessMonitor] COM initialization failed"); return false; } @@ -50,7 +50,7 @@ bool ProcessMonitor::InitializeWMI() if (FAILED(hr)) { - std::cerr << "[ProcessMonitor] WMI Locator creation failed" << std::endl; + WT_ERR("[ProcessMonitor] WMI Locator creation failed"); CoUninitialize(); return false; } @@ -68,14 +68,14 @@ bool ProcessMonitor::InitializeWMI() if (FAILED(hr)) { - std::cerr << "[ProcessMonitor] WMI Service connection failed" << std::endl; + WT_ERR("[ProcessMonitor] WMI Service connection failed"); pLocator->Release(); CoUninitialize(); return false; } wmiInitialized = true; - std::cout << "[ProcessMonitor] WMI initialized successfully" << std::endl; + WT_LOG("[ProcessMonitor] WMI initialized successfully"); return true; } @@ -155,7 +155,7 @@ void ProcessMonitor::CleanupWMI() } CoUninitialize(); wmiInitialized = false; - std::cout << "[ProcessMonitor] WMI cleaned up" << std::endl; + WT_LOG("[ProcessMonitor] WMI cleaned up"); } } @@ -168,7 +168,7 @@ std::vector ProcessMonitor::ScanUnityProcesses() const HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hSnapshot == INVALID_HANDLE_VALUE) { - std::cerr << "[ProcessMonitor] CreateToolhelp32Snapshot failed" << std::endl; + WT_ERR("[ProcessMonitor] CreateToolhelp32Snapshot failed"); return foundInstances; } @@ -197,7 +197,7 @@ std::vector ProcessMonitor::ScanUnityProcesses() CloseHandle(hSnapshot); - std::cout << "[ProcessMonitor] Scan complete. Found " << foundInstances.size() << " Unity instances" << std::endl; + WT_LOG("[ProcessMonitor] Scan complete. Found " << foundInstances.size() << " Unity instances"); return foundInstances; } @@ -207,7 +207,7 @@ std::string ProcessMonitor::GetProcessCommandLine(const DWORD pid) if (fullCommandLine.empty()) { - std::cout << "[ProcessMonitor] Could not get command line for PID " << pid << std::endl; + WT_LOG("[ProcessMonitor] Could not get command line for PID " << pid); return ""; } @@ -215,13 +215,13 @@ std::string ProcessMonitor::GetProcessCommandLine(const DWORD pid) if (projectPath.empty()) { - std::cout << "[ProcessMonitor] No -projectPath found in command line: " << fullCommandLine << std::endl; + WT_LOG("[ProcessMonitor] No -projectPath found in command line: " << fullCommandLine); return ""; } if (!IsUnityProject(projectPath)) { - std::cout << "[ProcessMonitor] Path is not a valid Unity project: " << projectPath << std::endl; + WT_LOG("[ProcessMonitor] Path is not a valid Unity project: " << projectPath); return ""; } diff --git a/src/tray_icon.cpp b/src/tray_icon.cpp index 261ade8..d852c30 100644 --- a/src/tray_icon.cpp +++ b/src/tray_icon.cpp @@ -16,31 +16,31 @@ TrayIcon::TrayIcon() : hwnd(nullptr), ZeroMemory(&nid, sizeof(NOTIFYICONDATAW)); ZeroMemory(&wc, sizeof(WNDCLASSW)); - std::cout << "[TrayIcon] Created" << std::endl; + WT_LOG("[TrayIcon] Created"); } TrayIcon::~TrayIcon() { Shutdown(); - std::cout << "[TrayIcon] Destroyed" << std::endl; + WT_LOG("[TrayIcon] Destroyed"); } bool TrayIcon::Initialize(const std::string &appName) { - std::cout << "[TrayIcon] Initializing: " << appName << std::endl; + WT_LOG("[TrayIcon] Initializing: " << appName); // 숨겨진 창 생성 if (!CreateHiddenWindow()) { - std::cerr << "[TrayIcon] Failed to create hidden window" << std::endl; + WT_ERR("[TrayIcon] Failed to create hidden window"); return false; } // 트레이 아이콘 생성 if (!CreateTrayIcon()) { - std::cerr << "[TrayIcon] Failed to create tray icon" << std::endl; + WT_ERR("[TrayIcon] Failed to create tray icon"); return false; } @@ -48,7 +48,7 @@ bool TrayIcon::Initialize(const std::string &appName) hMenu = CreateContextMenu(); if (!hMenu) { - std::cerr << "[TrayIcon] Failed to create context menu" << std::endl; + WT_ERR("[TrayIcon] Failed to create context menu"); return false; } @@ -56,7 +56,7 @@ bool TrayIcon::Initialize(const std::string &appName) UpdateTooltip(appName + " - Ready"); initialized = true; - std::cout << "[TrayIcon] Initialized successfully" << std::endl; + WT_LOG("[TrayIcon] Initialized successfully"); return true; } @@ -75,7 +75,7 @@ bool TrayIcon::CreateHiddenWindow() if (const DWORD error = GetLastError(); error != ERROR_CLASS_ALREADY_EXISTS) { // 이미 등록된 경우는 정상 - std::cerr << "[TrayIcon] RegisterClass failed (Error: " << error << ")" << std::endl; + WT_ERR("[TrayIcon] RegisterClass failed (Error: " << error << ")"); return false; } } @@ -98,13 +98,13 @@ bool TrayIcon::CreateHiddenWindow() if (!hwnd) { const DWORD error = GetLastError(); - std::cerr << "[TrayIcon] CreateWindow failed (Error: " << error << ")" << std::endl; + WT_ERR("[TrayIcon] CreateWindow failed (Error: " << error << ")"); return false; } WindowsDarkMode::ApplyToWindow(hwnd); - std::cout << "[TrayIcon] Hidden window created" << std::endl; + WT_LOG("[TrayIcon] Hidden window created"); return true; } @@ -121,20 +121,20 @@ bool TrayIcon::CreateTrayIcon() { if (!Shell_NotifyIconW(NIM_ADD, &nid)) { DWORD error = GetLastError(); - std::cerr << "[TrayIcon] Shell_NotifyIconW(NIM_ADD) failed (Error: " << error << ")" << std::endl; + WT_ERR("[TrayIcon] Shell_NotifyIconW(NIM_ADD) failed (Error: " << error << ")"); return false; } - std::cout << "[TrayIcon] Tray icon added to system tray" << std::endl; + WT_LOG("[TrayIcon] Tray icon added to system tray"); return true; } HICON TrayIcon::LoadPngIcon(const std::string& filePath) { - std::cout << "[TrayIcon] Loading PNG icon using WIC: " << filePath << std::endl; + WT_LOG("[TrayIcon] Loading PNG icon using WIC: " << filePath); // 파일 존재 확인 if (!fs::exists(filePath)) { - std::cerr << "[TrayIcon] PNG file not found: " << filePath << std::endl; + WT_ERR("[TrayIcon] PNG file not found: " << filePath); return LoadIcon(nullptr, IDI_APPLICATION); } @@ -153,13 +153,12 @@ HICON TrayIcon::LoadPngIcon(const std::string& filePath) { ); if (FAILED(hr)) { - std::cerr << "[TrayIcon] Failed to create WIC factory (HRESULT: 0x" - << std::hex << hr << ")" << std::endl; + WT_ERR("[TrayIcon] Failed to create WIC factory (HRESULT: 0x" << std::hex << hr << ")"); if (comInitialized) CoUninitialize(); return LoadIcon(nullptr, IDI_APPLICATION); } - std::cout << "[TrayIcon] ✅ WIC factory created successfully" << std::endl; + WT_LOG("[TrayIcon] ✅ WIC factory created successfully"); const std::wstring wFilePath(filePath.begin(), filePath.end()); @@ -174,8 +173,7 @@ HICON TrayIcon::LoadPngIcon(const std::string& filePath) { ); if (FAILED(hr)) { - std::cerr << "[TrayIcon] Failed to create PNG decoder (HRESULT: 0x" - << std::hex << hr << ")" << std::endl; + WT_ERR("[TrayIcon] Failed to create PNG decoder (HRESULT: 0x" << std::hex << hr << ")"); pFactory->Release(); if (comInitialized) CoUninitialize(); return LoadIcon(nullptr, IDI_APPLICATION); @@ -186,7 +184,7 @@ HICON TrayIcon::LoadPngIcon(const std::string& filePath) { hr = pDecoder->GetFrame(0, &pFrame); if (FAILED(hr)) { - std::cerr << "[TrayIcon] Failed to get frame from PNG" << std::endl; + WT_ERR("[TrayIcon] Failed to get frame from PNG"); pDecoder->Release(); pFactory->Release(); if (comInitialized) CoUninitialize(); @@ -196,13 +194,13 @@ HICON TrayIcon::LoadPngIcon(const std::string& filePath) { // 원본 크기 확인 UINT originalWidth, originalHeight; pFrame->GetSize(&originalWidth, &originalHeight); - std::cout << "[TrayIcon] Original PNG size: " << originalWidth << "x" << originalHeight << std::endl; + WT_LOG("[TrayIcon] Original PNG size: " << originalWidth << "x" << originalHeight); // 트레이 아이콘 크기 (시스템에서 권장하는 크기) int iconSize = GetSystemMetrics(SM_CXSMICON); // 보통 16x16 if (iconSize <= 0) iconSize = 32; // 기본값 - std::cout << "[TrayIcon] System tray icon size: " << iconSize << "x" << iconSize << std::endl; + WT_LOG("[TrayIcon] System tray icon size: " << iconSize << "x" << iconSize); // 스케일러 생성 (크기 조정) IWICBitmapScaler* pScaler = nullptr; @@ -280,18 +278,18 @@ HICON TrayIcon::LoadPngIcon(const std::string& filePath) { DeleteObject(hMaskBitmap); if (hIcon) { - std::cout << "[TrayIcon] ✅ PNG icon converted to HICON successfully!" << std::endl; + WT_LOG("[TrayIcon] ✅ PNG icon converted to HICON successfully!"); } else { DWORD error = GetLastError(); - std::cerr << "[TrayIcon] CreateIconIndirect failed (Error: " << error << ")" << std::endl; + WT_ERR("[TrayIcon] CreateIconIndirect failed (Error: " << error << ")"); } } else { - std::cerr << "[TrayIcon] Failed to copy pixels from WIC converter" << std::endl; + WT_ERR("[TrayIcon] Failed to copy pixels from WIC converter"); } DeleteObject(hBitmap); } else { - std::cerr << "[TrayIcon] Failed to create DIB section" << std::endl; + WT_ERR("[TrayIcon] Failed to create DIB section"); } } @@ -307,10 +305,10 @@ HICON TrayIcon::LoadPngIcon(const std::string& filePath) { } if (hIcon) { - std::cout << "[TrayIcon] ✅ PNG icon loaded successfully using WIC!" << std::endl; + WT_LOG("[TrayIcon] ✅ PNG icon loaded successfully using WIC!"); return hIcon; } else { - std::cerr << "[TrayIcon] ❌ Failed to load PNG, using fallback icon" << std::endl; + WT_ERR("[TrayIcon] ❌ Failed to load PNG, using fallback icon"); return LoadIcon(nullptr, IDI_APPLICATION); } } @@ -395,7 +393,7 @@ void TrayIcon::OpenGitHubRepository() { const auto githubUrl = "https://github.com/Snow0406/Unity-Wakatime"; const std::wstring wGithubUrl(githubUrl, githubUrl + strlen(githubUrl)); - std::cout << "[TrayIcon] Opening GitHub repository: " << githubUrl << std::endl; + WT_LOG("[TrayIcon] Opening GitHub repository: " << githubUrl); ShellExecuteW(nullptr, L"open", wGithubUrl.c_str(), nullptr, nullptr, SW_SHOWNORMAL); } @@ -427,7 +425,7 @@ void TrayIcon::ShowContextMenu(const int x, const int y) { if (!hMenu) { - std::cout << "[TrayIcon] hMenu is NULL!" << std::endl; + WT_LOG("[TrayIcon] hMenu is NULL!"); return; } @@ -521,7 +519,7 @@ std::string TrayIcon::ShowApiKeyInputDialog() { std::string currentApiKey = Globals::GetWakaTimeClient()->GetMaskedApiKey(); - std::cout << "[TrayIcon] Opening WakaTime API key page..." << std::endl; + WT_LOG("[TrayIcon] Opening WakaTime API key page..."); ShellExecuteW(nullptr, L"open", L"https://wakatime.com/api-key", nullptr, nullptr, SW_SHOWNORMAL); @@ -565,7 +563,7 @@ std::string TrayIcon::GetClipboardText() if (!OpenClipboard(hwnd)) { const DWORD error = GetLastError(); - std::cerr << "[TrayIcon] Failed to open clipboard (Error: " << error << ")" << std::endl; + WT_ERR("[TrayIcon] Failed to open clipboard (Error: " << error << ")"); return ""; } @@ -724,7 +722,7 @@ void TrayIcon::Shutdown() { if (!initialized) return; - std::cout << "[TrayIcon] Shutting down..." << std::endl; + WT_LOG("[TrayIcon] Shutting down..."); Shell_NotifyIconW(NIM_DELETE, &nid); @@ -747,7 +745,7 @@ void TrayIcon::Shutdown() } initialized = false; - std::cout << "[TrayIcon] Shutdown complete" << std::endl; + WT_LOG("[TrayIcon] Shutdown complete"); } #pragma region Notification diff --git a/src/wakatime_client.cpp b/src/wakatime_client.cpp index 1aca541..81b64ac 100644 --- a/src/wakatime_client.cpp +++ b/src/wakatime_client.cpp @@ -5,9 +5,22 @@ namespace constexpr size_t kMaxHeartbeatQueueSize = 256; constexpr auto kSameFileHeartbeatInterval = std::chrono::seconds(120); constexpr auto kSameFileWriteInterval = std::chrono::seconds(2); + + std::string WideToUtf8(const wchar_t *value) + { + if (value == nullptr || value[0] == L'\0') return ""; + + const int len = WideCharToMultiByte(CP_UTF8, 0, value, -1, nullptr, 0, nullptr, nullptr); + if (len <= 1) return ""; + + std::string result(len - 1, 0); + WideCharToMultiByte(CP_UTF8, 0, value, -1, &result[0], len, nullptr, nullptr); + return result; + } } WakaTimeClient::WakaTimeClient() : hSession(nullptr), + hConnect(nullptr), initialized(false), shouldStop(false), totalSent(0), @@ -16,12 +29,13 @@ WakaTimeClient::WakaTimeClient() : hSession(nullptr), userAgent = "unity-wakatime/1.0 (Windows)"; machineName = GetMachineName(); - std::cout << "[WakaTimeClient] Created for machine: " << machineName << std::endl; + WT_LOG("[WakaTimeClient] Created for machine: " << machineName); } WakaTimeClient::~WakaTimeClient() { shouldStop = true; + queueCv.notify_all(); if (senderThread.joinable()) { senderThread.join(); @@ -29,13 +43,12 @@ WakaTimeClient::~WakaTimeClient() CleanupHttpSession(); // HTTP 세션 정리 - std::cout << "[WakaTimeClient] Destroyed (Sent: " << totalSent.load() << ", Failed: " << totalFailed.load() << ")" - << std::endl; + WT_LOG("[WakaTimeClient] Destroyed (Sent: " << totalSent.load() << ", Failed: " << totalFailed.load() << ")"); } bool WakaTimeClient::Initialize(const std::string &providedApiKey) { - std::cout << "[WakaTimeClient] Initializing..." << std::endl; + WT_LOG("[WakaTimeClient] Initializing..."); if (!providedApiKey.empty()) // API 키 설정 { @@ -47,7 +60,7 @@ bool WakaTimeClient::Initialize(const std::string &providedApiKey) // 파일에서 로드 시도 if (!LoadApiKeyFromFile()) { - std::cerr << "[WakaTimeClient] No API key provided and failed to load from file" << std::endl; + WT_ERR("[WakaTimeClient] No API key provided and failed to load from file"); return false; } } @@ -55,7 +68,7 @@ bool WakaTimeClient::Initialize(const std::string &providedApiKey) // HTTP 세션 초기화 if (!InitializeHttpSession()) { - std::cerr << "[WakaTimeClient] Failed to initialize HTTP session" << std::endl; + WT_ERR("[WakaTimeClient] Failed to initialize HTTP session"); return false; } @@ -63,16 +76,17 @@ bool WakaTimeClient::Initialize(const std::string &providedApiKey) senderThread = std::thread(&WakaTimeClient::SenderThreadFunction, this); initialized = true; - std::cout << "[WakaTimeClient] Initialized successfully with API key" << std::endl; + WT_LOG("[WakaTimeClient] Initialized successfully with API key"); return true; } bool WakaTimeClient::ReInitialize(const std::string& newApiKey) { - std::cout << "[WakaTimeClient] Reinitializing with new API key..." << std::endl; + WT_LOG("[WakaTimeClient] Reinitializing with new API key..."); shouldStop = true; + queueCv.notify_all(); if (senderThread.joinable()) { senderThread.join(); @@ -101,21 +115,37 @@ bool WakaTimeClient::InitializeHttpSession() if (hSession == nullptr) { const DWORD error = GetLastError(); - std::cerr << "[WakaTimeClient] WinHttpOpen failed (Error: " << error << ")" << std::endl; + WT_ERR("[WakaTimeClient] WinHttpOpen failed (Error: " << error << ")"); + return false; + } + + // 연결 핸들을 한 번 생성해 heartbeat 간 재사용한다(keep-alive로 TLS 핸드셰이크 절감). + hConnect = WinHttpConnect(hSession, L"api.wakatime.com", INTERNET_DEFAULT_HTTPS_PORT, 0); + if (hConnect == nullptr) + { + const DWORD error = GetLastError(); + WT_ERR("[WakaTimeClient] WinHttpConnect failed (Error: " << error << ")"); + WinHttpCloseHandle(hSession); + hSession = nullptr; return false; } - std::cout << "[WakaTimeClient] HTTP session created" << std::endl; + WT_LOG("[WakaTimeClient] HTTP session created"); return true; } void WakaTimeClient::CleanupHttpSession() { + if (hConnect != nullptr) + { + WinHttpCloseHandle(hConnect); + hConnect = nullptr; + } if (hSession != nullptr) { WinHttpCloseHandle(hSession); hSession = nullptr; - std::cout << "[WakaTimeClient] HTTP session closed" << std::endl; + WT_LOG("[WakaTimeClient] HTTP session closed"); } } @@ -127,12 +157,7 @@ std::string WakaTimeClient::GetMachineName() if (GetComputerNameW(computerName, &size)) { // 와이드 문자를 일반 문자로 변환 - if (const int len = WideCharToMultiByte(CP_UTF8, 0, computerName, -1, nullptr, 0, nullptr, nullptr); len > 0) - { - std::string result(len - 1, 0); - WideCharToMultiByte(CP_UTF8, 0, computerName, -1, &result[0], len, nullptr, nullptr); - return result; - } + return WideToUtf8(computerName); } return "Unknown"; @@ -197,7 +222,6 @@ std::string WakaTimeClient::HeartbeatToJson(const HeartbeatData &heartbeat) << R"("language":")" << heartbeat.language << "\"," << R"("editor":")" << heartbeat.editor << "\"," << R"("operating_system":")" << heartbeat.operating_system << "\"," - << R"("machine":")" << EscapeJsonString(heartbeat.machine) << "\"," << R"("time":)" << heartbeat.time << "," << R"("is_write":)" << (heartbeat.is_write ? "true" : "false") << "}"; @@ -262,23 +286,20 @@ bool WakaTimeClient::SendHttpRequest(const std::string &jsonData) { if (!initialized || hSession == nullptr) { - std::cerr << "[WakaTimeClient] Not initialized" << std::endl; + WT_ERR("[WakaTimeClient] Not initialized"); return false; } - // WinHttpConnect: 서버 연결 - const HINTERNET hConnect = WinHttpConnect( - hSession, - L"api.wakatime.com", // 서버 주소 - INTERNET_DEFAULT_HTTPS_PORT, // HTTPS 포트 (443) - 0 - ); - + // 끊겼던 경우 재연결 if (hConnect == nullptr) { - const DWORD error = GetLastError(); - std::cerr << "[WakaTimeClient] WinHttpConnect failed (Error: " << error << ")" << std::endl; - return false; + hConnect = WinHttpConnect(hSession, L"api.wakatime.com", INTERNET_DEFAULT_HTTPS_PORT, 0); + if (hConnect == nullptr) + { + const DWORD error = GetLastError(); + WT_ERR("[WakaTimeClient] WinHttpConnect failed (Error: " << error << ")"); + return false; + } } // WinHttpOpenRequest: HTTP 요청 생성 @@ -295,8 +316,7 @@ bool WakaTimeClient::SendHttpRequest(const std::string &jsonData) if (hRequest == nullptr) { const DWORD error = GetLastError(); - std::cerr << "[WakaTimeClient] WinHttpOpenRequest failed (Error: " << error << ")" << std::endl; - WinHttpCloseHandle(hConnect); + WT_ERR("[WakaTimeClient] WinHttpOpenRequest failed (Error: " << error << ")"); return false; } @@ -304,15 +324,18 @@ bool WakaTimeClient::SendHttpRequest(const std::string &jsonData) std::string authHeader = "Authorization: Basic " + Base64Encode(apiKey + ":"); std::string contentTypeHeader = "Content-Type: application/json"; std::string userAgentHeader = "User-Agent: " + userAgent; + std::string machineHeader = "X-Machine-Name: " + machineName; // 와이드 문자로 변환해서 헤더 추가 const std::wstring wAuthHeader(authHeader.begin(), authHeader.end()); const std::wstring wContentTypeHeader(contentTypeHeader.begin(), contentTypeHeader.end()); const std::wstring wUserAgentHeader(userAgentHeader.begin(), userAgentHeader.end()); + const std::wstring wMachineHeader(machineHeader.begin(), machineHeader.end()); WinHttpAddRequestHeaders(hRequest, wAuthHeader.c_str(), -1, WINHTTP_ADDREQ_FLAG_ADD); WinHttpAddRequestHeaders(hRequest, wContentTypeHeader.c_str(), -1, WINHTTP_ADDREQ_FLAG_ADD); WinHttpAddRequestHeaders(hRequest, wUserAgentHeader.c_str(), -1, WINHTTP_ADDREQ_FLAG_ADD); + WinHttpAddRequestHeaders(hRequest, wMachineHeader.c_str(), -1, WINHTTP_ADDREQ_FLAG_ADD); // HTTP 요청 전송 const BOOL result = WinHttpSendRequest( @@ -326,6 +349,7 @@ bool WakaTimeClient::SendHttpRequest(const std::string &jsonData) ); bool success = false; + bool transportError = false; if (result) { @@ -351,67 +375,77 @@ bool WakaTimeClient::SendHttpRequest(const std::string &jsonData) else { ++totalFailed; - std::cout << "[WakaTimeClient] ❌ Heartbeat failed (HTTP " << statusCode << ")" << std::endl; + WT_LOG("[WakaTimeClient] ❌ Heartbeat failed (HTTP " << statusCode << ")"); } } } + else + { + transportError = true; + ++totalFailed; + WT_ERR("[WakaTimeClient] WinHttpReceiveResponse failed (Error: " << GetLastError() << ")"); + } } else { const DWORD error = GetLastError(); + transportError = true; ++totalFailed; - std::cerr << "[WakaTimeClient] WinHttpSendRequest failed (Error: " << error << ")" << std::endl; + WT_ERR("[WakaTimeClient] WinHttpSendRequest failed (Error: " << error << ")"); } WinHttpCloseHandle(hRequest); - WinHttpCloseHandle(hConnect); + + // 전송/수신 단계 실패는 연결이 끊겼을 수 있으므로 캐시한 연결을 폐기 → 다음 전송에서 재연결 + if (transportError && hConnect != nullptr) + { + WinHttpCloseHandle(hConnect); + hConnect = nullptr; + } return success; } void WakaTimeClient::SenderThreadFunction() { - std::cout << "[WakaTimeClient] Sender thread started" << std::endl; + WT_LOG("[WakaTimeClient] Sender thread started"); - while (!shouldStop) + while (true) { HeartbeatData heartbeat; - bool hasData = false; - // 큐에서 heartbeat 가져오기 + // 큐에 데이터가 들어오거나 종료될 때까지 블록 (busy-poll 제거) { - std::lock_guard lock(queueMutex); - if (!heartbeatQueue.empty()) + std::unique_lock lock(queueMutex); + queueCv.wait(lock, [this] { return shouldStop.load() || !heartbeatQueue.empty(); }); + + if (shouldStop && heartbeatQueue.empty()) { - heartbeat = heartbeatQueue.front(); - heartbeatQueue.pop(); - hasData = true; + break; } - } - if (hasData) - { - std::string jsonData = HeartbeatToJson(heartbeat); + heartbeat = heartbeatQueue.front(); + heartbeatQueue.pop(); + } - SendHttpRequest(jsonData); + const std::string jsonData = HeartbeatToJson(heartbeat); + SendHttpRequest(jsonData); - std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - } - else + // 연속 전송 레이트리밋. 종료 시 즉시 깨어나도록 wait_for 사용 { - // 큐가 비어있으면 잠시 대기 - std::this_thread::sleep_for(std::chrono::milliseconds(200)); + std::unique_lock lock(queueMutex); + queueCv.wait_for(lock, std::chrono::milliseconds(1000), [this] { return shouldStop.load(); }); } } - std::cout << "[WakaTimeClient] Sender thread stopped" << std::endl; + WT_LOG("[WakaTimeClient] Sender thread stopped"); } void WakaTimeClient::SendHeartbeat(const std::string &filePath, const std::string &projectName, const std::string& unityVersion, const bool isWrite) { if (!initialized) { - std::cerr << "[WakaTimeClient] Not initialized, cannot send heartbeat" << std::endl; + WT_ERR("[WakaTimeClient] Not initialized, cannot send heartbeat"); return; } @@ -420,7 +454,6 @@ void WakaTimeClient::SendHeartbeat(const std::string &filePath, const std::strin heartbeat.entity = filePath; heartbeat.project = projectName; heartbeat.language = "Unity"; - heartbeat.machine = machineName; heartbeat.time = GetUnixTimestamp(); heartbeat.is_write = isWrite; @@ -457,6 +490,7 @@ void WakaTimeClient::SendHeartbeat(const std::string &filePath, const std::strin lastQueuedAt = now; lastQueuedEntity = heartbeat.entity; lastQueuedProject = heartbeat.project; + queueCv.notify_one(); } } @@ -492,7 +526,7 @@ bool WakaTimeClient::LoadApiKeyFromFile() std::ifstream file(configPath); if (!file.is_open()) { - std::cout << "[WakaTimeClient] Config file not found: " << configPath << std::endl; + WT_LOG("[WakaTimeClient] Config file not found: " << configPath); return false; } @@ -503,11 +537,11 @@ bool WakaTimeClient::LoadApiKeyFromFile() if (apiKey.empty()) { - std::cout << "[WakaTimeClient] Empty API key in config file" << std::endl; + WT_LOG("[WakaTimeClient] Empty API key in config file"); return false; } - std::cout << "[WakaTimeClient] API key loaded from file: " << GetMaskedApiKey() << std::endl; + WT_LOG("[WakaTimeClient] API key loaded from file: " << GetMaskedApiKey()); return true; } @@ -518,24 +552,24 @@ bool WakaTimeClient::SaveApiKeyToFile(const std::string &key) std::ofstream file(configPath); if (!file.is_open()) { - std::cerr << "[WakaTimeClient] Failed to save API key to: " << configPath << std::endl; + WT_ERR("[WakaTimeClient] Failed to save API key to: " << configPath); return false; } file << key << std::endl; file.close(); - std::cout << "[WakaTimeClient] API key saved to: " << configPath << std::endl; + WT_LOG("[WakaTimeClient] API key saved to: " << configPath); return true; } void WakaTimeClient::FlushQueue() { - std::cout << "[WakaTimeClient] Flushing queue..." << std::endl; + WT_LOG("[WakaTimeClient] Flushing queue..."); if (const size_t remaining = GetQueueSize(); remaining == 0) { - std::cout << "[WakaTimeClient] Queue is empty" << std::endl; + WT_LOG("[WakaTimeClient] Queue is empty"); return; } @@ -547,13 +581,13 @@ void WakaTimeClient::FlushQueue() { if (auto elapsed = std::chrono::steady_clock::now() - startTime; elapsed > timeout) { - std::cout << "[WakaTimeClient] Flush timeout, " << GetQueueSize() << " items remaining" << std::endl; + WT_LOG("[WakaTimeClient] Flush timeout, " << GetQueueSize() << " items remaining"); break; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } - std::cout << "[WakaTimeClient] Queue flushed" << std::endl; + WT_LOG("[WakaTimeClient] Queue flushed"); } From bc1e72d6646808a8346598280b6faa0345dcbfac Mon Sep 17 00:00:00 2001 From: hy Date: Sat, 30 May 2026 13:42:31 +0900 Subject: [PATCH 2/2] feat: optimize --- CMakeLists.txt | 2 - include/file_watcher.h | 12 +- include/process_monitor.h | 71 ++----- include/tray_icon.h | 40 +++- include/unity_focus_detector.h | 6 +- main.cpp | 76 +++++--- src/file_watcher.cpp | 24 +++ src/process_monitor.cpp | 346 +++++++++++++++------------------ src/tray_icon.cpp | 62 ++++-- src/unity_focus_detector.cpp | 20 +- 10 files changed, 351 insertions(+), 308 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bbeff7d..ab50b2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,7 +50,6 @@ if(WIN32) winhttp shell32 psapi - wbemuuid ole32 oleaut32 windowscodecs @@ -63,7 +62,6 @@ if(WIN32) winhttp shell32 psapi - wbemuuid ole32 oleaut32 windowscodecs diff --git a/include/file_watcher.h b/include/file_watcher.h index ef147e2..7c90bed 100644 --- a/include/file_watcher.h +++ b/include/file_watcher.h @@ -55,9 +55,12 @@ class FileWatcher { mutable std::mutex projectsMutex; // 스레드 안전성을 위한 뮤텍스 std::deque pendingEvents; mutable std::mutex pendingEventsMutex; - + std::atomic notifyScheduled{false}; // PostMessage 코얼레싱 (큐 적재 통지 1회로 합침) + // 파일 변경 이벤트 콜백 함수 std::function changeCallback; + // 큐에 이벤트가 적재되었음을 메인 스레드에 통지 (PostMessage 등) + std::function notifyCallback; /** * 특정 프로젝트 폴더를 감시하는 워커 스레드 함수 @@ -96,6 +99,13 @@ class FileWatcher { * @param callback 파일이 변경될 때 호출될 함수 */ void SetChangeCallback(std::function callback); + + /** + * 큐 적재 통지 콜백 설정 (워커 스레드 → 메인 스레드 마샬링용). + * 워커 스레드가 이벤트를 큐에 넣으면 이 콜백을 호출한다(코얼레싱됨). + * @param callback 통지 시 호출될 함수 (예: PostMessage) + */ + void SetNotifyCallback(std::function callback); /** * Unity 프로젝트 감시 시작 diff --git a/include/process_monitor.h b/include/process_monitor.h index b8b14fa..644b1d1 100644 --- a/include/process_monitor.h +++ b/include/process_monitor.h @@ -1,60 +1,38 @@ #pragma once #include "globals.h" -#include // WMI 인터페이스 #include -#pragma comment(lib, "wbemuuid.lib") -#pragma comment(lib, "ole32.lib") -#pragma comment(lib, "oleaut32.lib") - /** * Unity 프로세스들 감지 */ class ProcessMonitor { private: std::unordered_map activeInstances; - IWbemLocator* pLocator = nullptr; - IWbemServices* pService = nullptr; - bool wmiInitialized = false; - - /** - * 문자열을 BSTR로 변환하는 헬퍼 함수 - * @param str 변환할 문자열 - * @return BSTR 포인터 (사용 후 SysFreeString으로 해제 필요) - */ - BSTR StringToBSTR(const std::wstring& str); - - /** - * BSTR을 문자열로 변환하는 헬퍼 함수 - * @param bstr 변환할 BSTR - * @return 변환된 문자열 - */ - std::string BSTRToString(BSTR bstr); /** - * WMI를 사용해서 실제 프로세스 커맨드 라인 가져오기 + * NtQueryInformationProcess + PEB 읽기로 프로세스 커맨드 라인을 가져온다. + * WMI/RPC를 거치지 않아 Wmiprvse를 깨우지 않고 비용이 낮다. + * (PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ 권한 필요, 64-bit 전제) * @param pid 대상 프로세스 ID - * @return 커맨드 라인 - */ - std::string GetRealCommandLine(DWORD pid); - - /** - * WMI 초기화 (COM 초기화) + * @return 커맨드 라인, 실패시 빈 문자열 */ - bool InitializeWMI(); + std::string GetCommandLineViaPeb(DWORD pid); /** - * WMI 정리 + * 특정 프로세스의 커맨드 라인을 가져와 Unity 프로젝트 경로로 해석 + * @param pid 대상 프로세스 ID + * @return 프로젝트 경로, 실패시 빈문자열 */ - void CleanupWMI(); + std::string GetProcessCommandLine(DWORD pid); /** - * 특정 프로세스의 커맨드 라인을 가져오기 + * 스냅샷 엔트리가 Unity 프로세스이면 인스턴스 정보로 해석 * @param pid 대상 프로세스 ID - * @return 커맨드 라인, 실패시 빈문자열 + * @param instance 해석된 인스턴스 (출력) + * @return Unity 프로젝트로 해석되면 true */ - std::string GetProcessCommandLine(DWORD pid); + bool ResolveUnityInstance(DWORD pid, UnityInstance& instance); /** * 커맨드 라인에서 Unity 프로젝트 경로 추출 @@ -96,22 +74,19 @@ class ProcessMonitor { ~ProcessMonitor(); /** - * 현재 실행 중인 모든 Unity 인스턴스를 스캔 + * 현재 실행 중인 모든 Unity 인스턴스를 스캔 (초기 스캔용). + * 결과를 activeInstances에도 등록하여 이후 PollChanges가 중복 보고하지 않도록 한다. * @return 발견된 Unity 인스턴스들 */ std::vector ScanUnityProcesses(); /** - * 새로 시작된 Unity 프로세스가 있는지 확인 - * @return 새로운 인스턴스들 - */ - std::vector GetNewInstances(); - - /** - * 종료된 Unity 프로세스가 있는지 확인 - * @return 종료된 인스턴스들 + * 단일 스냅샷으로 새로 시작/종료된 Unity 프로세스를 한 번에 diff한다. + * 이미 알려진 PID는 재해석(PEB/디스크 조회)하지 않는다. + * @param started 새로 감지된 인스턴스 (출력) + * @param closed 종료된 인스턴스 (출력) */ - std::vector GetClosedInstances(); + void PollChanges(std::vector& started, std::vector& closed); /** * 특정 프로세스 ID가 실행 중인지 확인 @@ -119,10 +94,4 @@ class ProcessMonitor { * @return 실행중이면 true */ bool IsProcessRunning(DWORD processId); - - /** - * 현재 활성화된 모든 Unity 인스턴스 반환 - * @return 현재 실행중인 인스턴스들 - */ - const std::unordered_map& GetActiveInstances() const; }; \ No newline at end of file diff --git a/include/tray_icon.h b/include/tray_icon.h index 29fcc0b..b058fea 100644 --- a/include/tray_icon.h +++ b/include/tray_icon.h @@ -8,6 +8,7 @@ // 트레이 아이콘 관련 상수들 #define WM_TRAYICON (WM_USER + 1) // 트레이 아이콘 메시지 +#define WM_APP_FILE_EVENT (WM_APP + 1) // 파일 변경 큐 적재 통지 (워커 스레드 → 메인 스레드) #define IDM_EXIT 100 // 종료 메뉴 ID #define IDM_SHOW_STATUS 101 // 상태 보기 메뉴 ID #define IDM_TOGGLE_MONITORING 102 // 모니터링 토글 메뉴 ID @@ -15,6 +16,12 @@ #define IDM_SETTINGS 104 // 설정 메뉴 ID #define IDM_GITHUB 105 // Github 링크 +// 타이머 ID 및 주기 (메시지 펌프 기반 이벤트화) +#define TIMER_PROCESS_SCAN 1 // Unity 프로세스 생성/종료 스캔 +#define TIMER_PERIODIC_HEARTBEAT 2 // 포커스 유지 시 주기 heartbeat +#define PROCESS_SCAN_INTERVAL_MS 10000 // 10초 +#define PERIODIC_HEARTBEAT_INTERVAL_MS 120000 // 2분 + /** * Windows 시스템 트레이에 아이콘을 표시하고 사용자 인터랙션 처리 */ @@ -39,6 +46,11 @@ class TrayIcon { std::function onOpenDashboard; // 대시보드 열기 콜백 std::function onShowSettings; // 설정 보기 콜백 std::function onApiKeyChange; // API 키 변경 콜백 + + // 이벤트 허브 콜백 (메시지 펌프에서 디스패치) + std::function onFileEvent; // WM_APP_FILE_EVENT → 파일 이벤트 드레인 + std::function onProcessScan; // TIMER_PROCESS_SCAN → 프로세스 생성/종료 스캔 + std::function onPeriodicTick; // TIMER_PERIODIC_HEARTBEAT → 주기 heartbeat 체크 /** * 숨겨진 창 생성 (트레이 아이콘 메시지 수신용) @@ -170,10 +182,17 @@ class TrayIcon { void Shutdown(); /** - * 메시지 처리 (메인 스레드에서 호출) - * @return 처리된 메시지 수 + * 메시지 펌프 실행 (메인 스레드에서 호출, WM_QUIT까지 블록). + * 진입 시 프로세스 스캔/주기 heartbeat 타이머를 설치하고 종료 시 정리한다. + * @return WM_QUIT의 exit code */ - int ProcessMessages(); + int RunMessageLoop(); + + /** + * 파일 변경 큐 적재를 메인 스레드에 통지 (워커 스레드에서 호출 가능). + * 메시지를 post하여 메시지 펌프가 WM_APP_FILE_EVENT로 깨어나도록 한다. + */ + void NotifyFileEvent(); /** * 상태 정보 업데이트 (서브메뉴에 반영) @@ -223,6 +242,21 @@ class TrayIcon { */ void SetApiKeyChangeCallback(const std::function &callback); + /** + * 파일 변경 이벤트 처리 콜백 설정 (WM_APP_FILE_EVENT) + */ + void SetFileEventCallback(const std::function &callback); + + /** + * 프로세스 스캔 콜백 설정 (TIMER_PROCESS_SCAN) + */ + void SetProcessScanCallback(const std::function &callback); + + /** + * 주기 heartbeat 틱 콜백 설정 (TIMER_PERIODIC_HEARTBEAT) + */ + void SetPeriodicTickCallback(const std::function &callback); + private: /** * 정적 윈도우 프로시저 - Windows API 콜백용 diff --git a/include/unity_focus_detector.h b/include/unity_focus_detector.h index 1935ad8..f715ad4 100644 --- a/include/unity_focus_detector.h +++ b/include/unity_focus_detector.h @@ -13,9 +13,11 @@ class UnityFocusDetector { public: /** - * 포커스 상태 확인 + * 포그라운드 창 변경 시 호출 (SetWinEventHook 콜백에서 구동). + * 클래스명에 "Unity"가 포함되면 포커스 전이로 판정한다. + * @param hwnd 새 포그라운드 창 핸들 (nullptr 가능) */ - void CheckFocused(); + void OnForegroundChanged(HWND hwnd); /** * 2분마다 호출 diff --git a/main.cpp b/main.cpp index e55e970..706b5b8 100644 --- a/main.cpp +++ b/main.cpp @@ -58,6 +58,9 @@ void OnTrayExit() { WT_LOG("[Main] Exit requested from tray"); Globals::RequestExit(); + // 메시지 펌프(GetMessage)를 깨워 정상 종료시킨다. 트레이 메뉴 핸들러는 + // 메인 스레드(WndProc)에서 동기 호출되므로 여기서 PostQuitMessage가 안전하다. + PostQuitMessage(0); } void OnTrayShowStatus() @@ -241,6 +244,15 @@ void InitialUnityProjectScan() WT_LOG("[Main] Initial scan complete. Watching " << instances.size() << " Unity projects"); } +// 포그라운드 창 변경 이벤트 콜백 (SetWinEventHook). +// WINEVENT_OUTOFCONTEXT라 메인 스레드의 메시지 펌프 중 디스패치되므로 마샬링 불필요. +void CALLBACK FocusWinEventProc(HWINEVENTHOOK, const DWORD event, const HWND hwnd, + const LONG idObject, LONG, DWORD, DWORD) +{ + if (event != EVENT_SYSTEM_FOREGROUND || idObject != OBJID_WINDOW) return; + if (g_unityFocusDetector) g_unityFocusDetector->OnForegroundChanged(hwnd); +} + int main() { WT_LOG("[Main] Unity WakaTime Monitor Starting..."); @@ -282,6 +294,11 @@ int main() g_fileWatcher = &fileWatcher; fileWatcher.SetChangeCallback(OnFileChanged); + // 워커 스레드의 파일 변경 → 메인 스레드로 PostMessage 마샬링 (InitialScan 이전에 설치) + fileWatcher.SetNotifyCallback([&trayIcon]() + { + trayIcon.NotifyFileEvent(); + }); UnityFocusDetector unityFocusDetector; g_unityFocusDetector = &unityFocusDetector; @@ -296,43 +313,40 @@ int main() WT_LOG("\n[Main] Unity WakaTime is now running in background!"); - auto lastScan = std::chrono::steady_clock::now(); - const auto scanInterval = std::chrono::seconds(10); - - while (!Globals::ShouldExit()) + // 이벤트 허브 콜백 배선 (TrayIcon의 메시지 펌프에서 디스패치) + trayIcon.SetFileEventCallback([]() { - if (g_fileWatcher) - { - g_fileWatcher->DrainPendingEvents(); - } + if (g_fileWatcher) g_fileWatcher->DrainPendingEvents(); + }); - if (g_unityFocusDetector) { - g_unityFocusDetector->CheckFocused(); - g_unityFocusDetector->SendPeriodicHeartbeat(); - } + trayIcon.SetProcessScanCallback([&processMonitor]() + { + std::vector started; + std::vector closed; + processMonitor.PollChanges(started, closed); + if (!started.empty()) HandleNewUnityInstances(started); + if (!closed.empty()) HandleClosedUnityInstances(closed); + }); + + trayIcon.SetPeriodicTickCallback([]() + { + if (g_unityFocusDetector) g_unityFocusDetector->SendPeriodicHeartbeat(); + }); - int msgCount = trayIcon.ProcessMessages(); - if (msgCount > 5) std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 많은 메시지 → 빠른 처리 - else if (msgCount > 0) std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 일부 메시지 → 보통 처리 - else std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // 메시지 없음 → 여유 있게 + // 포커스 추적: SetWinEventHook(OUTOFCONTEXT) → 콜백이 메인 펌프에서 디스패치됨 (매초 폴링 제거) + const HWINEVENTHOOK focusHook = SetWinEventHook( + EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, + nullptr, FocusWinEventProc, 0, 0, + WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS); - if (auto now = std::chrono::steady_clock::now(); now - lastScan >= scanInterval) - { - // 새로운 Unity 인스턴스 감지 - if (auto newInstances = processMonitor.GetNewInstances(); !newInstances.empty()) - { - HandleNewUnityInstances(newInstances); - } + // 훅 설치 시점에 이미 Unity가 포그라운드일 수 있으므로 초기 상태 1회 캡처 + unityFocusDetector.OnForegroundChanged(GetForegroundWindow()); - // 종료된 Unity 인스턴스 감지 - if (auto closedInstances = processMonitor.GetClosedInstances(); !closedInstances.empty()) - { - HandleClosedUnityInstances(closedInstances); - } + // 메시지 펌프: idle 시 GetMessage가 커널에서 블록되어 CPU ≈ 0, + // 파일/포커스/타이머/트레이 이벤트에만 깨어난다. WM_QUIT까지 블록. + trayIcon.RunMessageLoop(); - lastScan = now; - } - } + if (focusHook) UnhookWinEvent(focusHook); WT_LOG("\n[Main] Shutting down Unity WakaTime..."); trayIcon.ShowInfoNotification("Unity WakaTime shutting down..."); diff --git a/src/file_watcher.cpp b/src/file_watcher.cpp index 5670a3c..6522b02 100644 --- a/src/file_watcher.cpp +++ b/src/file_watcher.cpp @@ -24,6 +24,12 @@ void FileWatcher::SetChangeCallback(std::function WT_LOG("[FileWatcher] Change callback set"); } +void FileWatcher::SetNotifyCallback(std::function callback) +{ + notifyCallback = std::move(callback); + WT_LOG("[FileWatcher] Notify callback set"); +} + bool FileWatcher::StartWatching(const std::string &projectPath, const std::string &projectName, const std::string &unityVersion) { std::lock_guard lock(projectsMutex); @@ -265,6 +271,12 @@ void FileWatcher::ProcessFileChanges(char *buffer, DWORD bytesReturned, WatchedP } pendingEvents.emplace_back(std::move(event)); } + + // 메인 스레드에 통지 (코얼레싱: 이미 예약돼 있으면 추가 post 생략) + if (notifyCallback && !notifyScheduled.exchange(true)) + { + notifyCallback(); + } } } @@ -394,7 +406,11 @@ void FileWatcher::DrainPendingEvents(const size_t maxEvents) return; } + // 드레인 시작 전에 예약 플래그를 내려, 이 시점 이후 도착하는 이벤트는 새 통지를 post하도록 한다. + notifyScheduled.store(false); + std::vector localEvents; + bool moreRemaining = false; { std::lock_guard lock(pendingEventsMutex); if (pendingEvents.empty()) @@ -409,12 +425,20 @@ void FileWatcher::DrainPendingEvents(const size_t maxEvents) localEvents.emplace_back(std::move(pendingEvents.front())); pendingEvents.pop_front(); } + + moreRemaining = !pendingEvents.empty(); } for (const auto &event: localEvents) { changeCallback(event); } + + // maxEvents 한도로 다 비우지 못했으면 다음 처리를 위해 통지를 다시 예약 + if (moreRemaining && notifyCallback && !notifyScheduled.exchange(true)) + { + notifyCallback(); + } } size_t FileWatcher::GetWatchedProjectCount() const diff --git a/src/process_monitor.cpp b/src/process_monitor.cpp index 5df5252..b4af22f 100644 --- a/src/process_monitor.cpp +++ b/src/process_monitor.cpp @@ -1,5 +1,27 @@ #include "process_monitor.h" #include // Windows 프로세스 스냅샷 API +#include // NtQueryInformationProcess, PEB, RTL_USER_PROCESS_PARAMETERS + +namespace +{ + // ntdll의 NtQueryInformationProcess를 동적 로드해 ntdll 링크 의존을 피한다. + using NtQueryInformationProcess_t = NTSTATUS(NTAPI *)( + HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG); + + NtQueryInformationProcess_t GetNtQueryInformationProcess() + { + static const auto fn = []() -> NtQueryInformationProcess_t + { + if (const HMODULE ntdll = GetModuleHandleW(L"ntdll.dll")) + { + return reinterpret_cast( + reinterpret_cast(GetProcAddress(ntdll, "NtQueryInformationProcess"))); + } + return nullptr; + }(); + return fn; + } +} ProcessMonitor::ProcessMonitor() { @@ -8,155 +30,115 @@ ProcessMonitor::ProcessMonitor() ProcessMonitor::~ProcessMonitor() { - CleanupWMI(); activeInstances.clear(); WT_LOG("[ProcessMonitor] Destroyed"); } -BSTR ProcessMonitor::StringToBSTR(const std::wstring &str) -{ - return SysAllocString(str.c_str()); -} - -std::string ProcessMonitor::BSTRToString(const BSTR bstr) -{ - if (bstr == nullptr) return ""; - - const int len = WideCharToMultiByte(CP_UTF8, 0, bstr, -1, nullptr, 0, nullptr, nullptr); - if (len <= 0) return ""; - - std::string result(len - 1, 0); - WideCharToMultiByte(CP_UTF8, 0, bstr, -1, &result[0], len, nullptr, nullptr); - - return result; -} - -bool ProcessMonitor::InitializeWMI() +std::string ProcessMonitor::GetCommandLineViaPeb(const DWORD pid) { - HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); - if (FAILED(hr)) - { - WT_ERR("[ProcessMonitor] COM initialization failed"); - return false; - } + const auto NtQueryInformationProcess = GetNtQueryInformationProcess(); + if (NtQueryInformationProcess == nullptr) return ""; - hr = CoInitializeSecurity( - nullptr, -1, nullptr, nullptr, - RPC_C_AUTHN_LEVEL_NONE, RPC_C_IMP_LEVEL_IMPERSONATE, - nullptr, EOAC_NONE, nullptr); + const HANDLE hProcess = OpenProcess( + PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ, FALSE, pid); + if (hProcess == nullptr) return ""; - hr = CoCreateInstance(CLSID_WbemLocator, nullptr, - CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID *) &pLocator); + std::string commandLine; - if (FAILED(hr)) + do { - WT_ERR("[ProcessMonitor] WMI Locator creation failed"); - CoUninitialize(); - return false; - } + // 1) PEB 주소 조회 + PROCESS_BASIC_INFORMATION pbi; + ZeroMemory(&pbi, sizeof(pbi)); + ULONG returnLength = 0; + if (const NTSTATUS status = NtQueryInformationProcess( + hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &returnLength); + status < 0 || pbi.PebBaseAddress == nullptr) + { + break; + } - const BSTR strNetworkResource = StringToBSTR(L"ROOT\\CIMV2"); + // 2) PEB 읽기 → ProcessParameters 포인터 + PEB peb; + ZeroMemory(&peb, sizeof(peb)); + if (!ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), nullptr) || + peb.ProcessParameters == nullptr) + { + break; + } - hr = pLocator->ConnectServer( - strNetworkResource, - nullptr, nullptr, nullptr, - 0, nullptr, nullptr, - &pService - ); + // 3) RTL_USER_PROCESS_PARAMETERS 읽기 → CommandLine(UNICODE_STRING) + RTL_USER_PROCESS_PARAMETERS params; + ZeroMemory(¶ms, sizeof(params)); + if (!ReadProcessMemory(hProcess, peb.ProcessParameters, ¶ms, sizeof(params), nullptr) || + params.CommandLine.Buffer == nullptr || + params.CommandLine.Length == 0) + { + break; + } - SysFreeString(strNetworkResource); + // 4) CommandLine 버퍼 읽기 (Length는 바이트 단위) + const USHORT byteLen = params.CommandLine.Length; + std::wstring wCommandLine(byteLen / sizeof(WCHAR), L'\0'); + if (!ReadProcessMemory(hProcess, params.CommandLine.Buffer, + wCommandLine.data(), byteLen, nullptr)) + { + break; + } - if (FAILED(hr)) - { - WT_ERR("[ProcessMonitor] WMI Service connection failed"); - pLocator->Release(); - CoUninitialize(); - return false; - } + // 5) UTF-8 변환 + if (const int len = WideCharToMultiByte(CP_UTF8, 0, wCommandLine.c_str(), + static_cast(wCommandLine.size()), + nullptr, 0, nullptr, nullptr); len > 0) + { + commandLine.resize(len); + WideCharToMultiByte(CP_UTF8, 0, wCommandLine.c_str(), + static_cast(wCommandLine.size()), + commandLine.data(), len, nullptr, nullptr); + } + } while (false); - wmiInitialized = true; - WT_LOG("[ProcessMonitor] WMI initialized successfully"); - return true; + CloseHandle(hProcess); + return commandLine; } -std::string ProcessMonitor::GetRealCommandLine(const DWORD pid) +std::string ProcessMonitor::GetProcessCommandLine(const DWORD pid) { - if (!wmiInitialized && !InitializeWMI()) return ""; - - // WQL 쿼리 생성 - 특정 프로세스 ID의 커맨드 라인 가져오기 - std::string commandLine; - const std::wstring query = L"SELECT CommandLine FROM Win32_Process WHERE ProcessId = " + std::to_wstring(pid); - - IEnumWbemClassObject *pEnumerator = nullptr; + const std::string fullCommandLine = GetCommandLineViaPeb(pid); - const BSTR strQueryLanguage = StringToBSTR(L"WQL"); - const BSTR strQuery = StringToBSTR(query); - - // 쿼리 실행 - HRESULT hr = pService->ExecQuery( - strQueryLanguage, - strQuery, - WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, - nullptr, - &pEnumerator); - - SysFreeString(strQueryLanguage); - SysFreeString(strQuery); - - if (SUCCEEDED(hr)) + if (fullCommandLine.empty()) { - IWbemClassObject *pObj = nullptr; - ULONG returnValue = 0; - - // 결과 가져오기 - hr = pEnumerator->Next(WBEM_INFINITE, 1, &pObj, &returnValue); - if (SUCCEEDED(hr) && returnValue > 0) - { - VARIANT cmdLineVariant; - VariantInit(&cmdLineVariant); + WT_LOG("[ProcessMonitor] Could not get command line for PID " << pid); + return ""; + } - // CommandLine 속성 가져오기 - hr = pObj->Get(L"CommandLine", 0, &cmdLineVariant, nullptr, nullptr); - if (SUCCEEDED(hr) && cmdLineVariant.vt == VT_BSTR && cmdLineVariant.bstrVal != nullptr) - { - const int len = WideCharToMultiByte(CP_UTF8, 0, cmdLineVariant.bstrVal, -1, - nullptr, 0, nullptr, nullptr); - if (len > 0) - { - commandLine.resize(len - 1); - WideCharToMultiByte(CP_UTF8, 0, cmdLineVariant.bstrVal, -1, &commandLine[0], - len, nullptr, nullptr); - } - } + std::string projectPath = ExtractProjectPath(fullCommandLine); - VariantClear(&cmdLineVariant); - pObj->Release(); - } + if (projectPath.empty()) + { + WT_LOG("[ProcessMonitor] No -projectPath found in command line: " << fullCommandLine); + return ""; + } - pEnumerator->Release(); + if (!IsUnityProject(projectPath)) + { + WT_LOG("[ProcessMonitor] Path is not a valid Unity project: " << projectPath); + return ""; } - return commandLine; + return projectPath; } -void ProcessMonitor::CleanupWMI() +bool ProcessMonitor::ResolveUnityInstance(const DWORD pid, UnityInstance& instance) { - if (wmiInitialized) - { - if (pService) - { - pService->Release(); - pService = nullptr; - } - if (pLocator) - { - pLocator->Release(); - pLocator = nullptr; - } - CoUninitialize(); - wmiInitialized = false; - WT_LOG("[ProcessMonitor] WMI cleaned up"); - } + std::string projectPath = GetProcessCommandLine(pid); + if (projectPath.empty()) return false; + + instance.processId = pid; + instance.projectName = GetProjectName(projectPath); + instance.editorVersion = GetUnityEditorVersion(projectPath); + instance.projectPath = std::move(projectPath); + return true; } std::vector ProcessMonitor::ScanUnityProcesses() @@ -181,15 +163,10 @@ std::vector ProcessMonitor::ScanUnityProcesses() { if (_wcsicmp(entry.szExeFile, L"Unity.exe") == 0 || _wcsicmp(entry.szExeFile, L"Unity") == 0) { - if (std::string projectPath = GetProcessCommandLine(entry.th32ProcessID); !projectPath.empty()) + if (UnityInstance instance; ResolveUnityInstance(entry.th32ProcessID, instance)) { - UnityInstance instance; - instance.processId = entry.th32ProcessID; - instance.projectPath = projectPath; - instance.projectName = GetProjectName(projectPath); - instance.editorVersion = GetUnityEditorVersion(projectPath); - - foundInstances.push_back(instance); + activeInstances[instance.processId] = instance; + foundInstances.push_back(std::move(instance)); } } } while (Process32NextW(hSnapshot, &entry)); @@ -201,31 +178,57 @@ std::vector ProcessMonitor::ScanUnityProcesses() return foundInstances; } -std::string ProcessMonitor::GetProcessCommandLine(const DWORD pid) +void ProcessMonitor::PollChanges(std::vector& started, std::vector& closed) { - const std::string fullCommandLine = GetRealCommandLine(pid); - - if (fullCommandLine.empty()) + const HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (hSnapshot == INVALID_HANDLE_VALUE) { - WT_LOG("[ProcessMonitor] Could not get command line for PID " << pid); - return ""; + WT_ERR("[ProcessMonitor] CreateToolhelp32Snapshot failed"); + return; } - std::string projectPath = ExtractProjectPath(fullCommandLine); + // 1) 현재 살아 있는 Unity PID 집합 수집 (exe 이름만 비교 — 저비용) + std::unordered_set currentUnityPids; - if (projectPath.empty()) + PROCESSENTRY32W entry; + entry.dwSize = sizeof(PROCESSENTRY32W); + + if (Process32FirstW(hSnapshot, &entry)) { - WT_LOG("[ProcessMonitor] No -projectPath found in command line: " << fullCommandLine); - return ""; + do + { + if (_wcsicmp(entry.szExeFile, L"Unity.exe") == 0 || _wcsicmp(entry.szExeFile, L"Unity") == 0) + { + currentUnityPids.insert(entry.th32ProcessID); + } + } while (Process32NextW(hSnapshot, &entry)); } - if (!IsUnityProject(projectPath)) + CloseHandle(hSnapshot); + + // 2) 새로 등장한 PID만 비싼 해석 수행 + for (const DWORD pid : currentUnityPids) { - WT_LOG("[ProcessMonitor] Path is not a valid Unity project: " << projectPath); - return ""; + if (activeInstances.find(pid) != activeInstances.end()) continue; // 이미 알려진 PID는 재조회 생략 + + if (UnityInstance instance; ResolveUnityInstance(pid, instance)) + { + activeInstances[pid] = instance; + started.push_back(std::move(instance)); + } } - return projectPath; + // 3) 더 이상 보이지 않는 PID는 종료된 것으로 판정 (동일 스냅샷으로 diff) + auto it = activeInstances.begin(); + while (it != activeInstances.end()) + { + if (currentUnityPids.find(it->first) == currentUnityPids.end()) + { + closed.push_back(it->second); + it = activeInstances.erase(it); + } + else ++it; + } } std::string ProcessMonitor::ExtractProjectPath(const std::string &commandLine) @@ -233,22 +236,13 @@ std::string ProcessMonitor::ExtractProjectPath(const std::string &commandLine) std::string lowerCommandLine = commandLine; std::transform(lowerCommandLine.begin(), lowerCommandLine.end(), lowerCommandLine.begin(), tolower); - size_t pos = lowerCommandLine.find("-projectpath"); + constexpr char projectPathArg[] = "-projectpath"; + size_t pos = lowerCommandLine.find(projectPathArg); if (pos == std::string::npos) return ""; - // 원래 문자열에서 해당 위치 찾기 - pos = commandLine.find("-projectpath", pos - (lowerCommandLine.length() - commandLine.length())); - if (pos == std::string::npos) - { - pos = commandLine.find("-projectpath", pos - (lowerCommandLine.length() - commandLine.length())); - } - - // -projectPath 다음 공백이나 탭 찾기 - pos = commandLine.find_first_of(" \t", pos); - if (pos == std::string::npos) - { - return ""; - } + // lowerCommandLine and commandLine have identical lengths, so the index can + // be reused to parse the original-cased command line. + pos += sizeof(projectPathArg) - 1; // 공백/탭 건너뛰기 pos = commandLine.find_first_not_of(" \t", pos); @@ -341,40 +335,6 @@ std::string ProcessMonitor::ParseProjectVersionFile(const std::string &versionFi return ""; } -std::vector ProcessMonitor::GetNewInstances() -{ - const std::vector currentInstances = ScanUnityProcesses(); - std::vector newInstances; - - for (const auto &instance: currentInstances) - { - if (activeInstances.find(instance.processId) == activeInstances.end()) - { - newInstances.push_back(instance); - activeInstances[instance.processId] = instance; - } - } - - return newInstances; -} - -std::vector ProcessMonitor::GetClosedInstances() -{ - std::vector closedInstances; - - auto it = activeInstances.begin(); - while (it != activeInstances.end()) - { - if (!IsProcessRunning(it->first)) - { - closedInstances.push_back(it->second); - it = activeInstances.erase(it); - } else ++it; - } - - return closedInstances; -} - bool ProcessMonitor::IsUnityProject(const std::string &projectPath) { return fs::exists(projectPath + "\\Assets") && fs::exists(projectPath + "\\ProjectSettings"); @@ -382,7 +342,7 @@ bool ProcessMonitor::IsUnityProject(const std::string &projectPath) bool ProcessMonitor::IsProcessRunning(const DWORD pid) { - const HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); + const HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); if (hProcess == nullptr) return false; DWORD exitCode; diff --git a/src/tray_icon.cpp b/src/tray_icon.cpp index d852c30..ccaabab 100644 --- a/src/tray_icon.cpp +++ b/src/tray_icon.cpp @@ -492,27 +492,35 @@ void TrayIcon::HandleMenuCommand(int menuId) } -int TrayIcon::ProcessMessages() +int TrayIcon::RunMessageLoop() { if (!initialized) return 0; - MSG msg; - int processedCount = 0; + // 이벤트화 타이머 설치: 프로세스 스캔(10초), 포커스 유지 시 주기 heartbeat(2분) + SetTimer(hwnd, TIMER_PROCESS_SCAN, PROCESS_SCAN_INTERVAL_MS, nullptr); + SetTimer(hwnd, TIMER_PERIODIC_HEARTBEAT, PERIODIC_HEARTBEAT_INTERVAL_MS, nullptr); - // PeekMessage로 논블로킹 방식으로 메시지 처리 - while (PeekMessage(&msg, hwnd, 0, 0, PM_REMOVE)) + // 정석 메시지 펌프: idle 시 GetMessage가 커널에서 블록되어 CPU ≈ 0. + // hwnd=nullptr → 스레드 메시지(WM_QUIT)와 WinEvent 훅 콜백까지 모두 수신/디스패치. + MSG msg; + BOOL result; + while ((result = GetMessage(&msg, nullptr, 0, 0)) != 0) { - processedCount++; + if (result == -1) break; // GetMessage 오류 TranslateMessage(&msg); DispatchMessage(&msg); - - if (msg.message == WM_QUIT) - { - break; - } } - return processedCount; + KillTimer(hwnd, TIMER_PROCESS_SCAN); + KillTimer(hwnd, TIMER_PERIODIC_HEARTBEAT); + + return static_cast(msg.wParam); +} + +void TrayIcon::NotifyFileEvent() +{ + if (!initialized || hwnd == nullptr) return; + PostMessage(hwnd, WM_APP_FILE_EVENT, 0, 0); } std::string TrayIcon::ShowApiKeyInputDialog() @@ -641,6 +649,21 @@ LRESULT TrayIcon::HandleWindowMessage(const HWND hwnd, const UINT msg, const WPA HandleMenuCommand(LOWORD(wParam)); return 0; + case WM_APP_FILE_EVENT: + if (onFileEvent) onFileEvent(); + return 0; + + case WM_TIMER: + if (wParam == TIMER_PROCESS_SCAN) + { + if (onProcessScan) onProcessScan(); + } + else if (wParam == TIMER_PERIODIC_HEARTBEAT) + { + if (onPeriodicTick) onPeriodicTick(); + } + return 0; + case WM_DESTROY: return 0; // PostQuitMessage 제거 @@ -794,4 +817,19 @@ void TrayIcon::SetApiKeyChangeCallback(const std::function &callback) +{ + onFileEvent = callback; +} + +void TrayIcon::SetProcessScanCallback(const std::function &callback) +{ + onProcessScan = callback; +} + +void TrayIcon::SetPeriodicTickCallback(const std::function &callback) +{ + onPeriodicTick = callback; +} + #pragma endregion Callbacks diff --git a/src/unity_focus_detector.cpp b/src/unity_focus_detector.cpp index 28f7f07..8984298 100644 --- a/src/unity_focus_detector.cpp +++ b/src/unity_focus_detector.cpp @@ -2,30 +2,24 @@ #include -void UnityFocusDetector::CheckFocused() +void UnityFocusDetector::OnForegroundChanged(const HWND hwnd) { - const HWND focusedWindow = GetForegroundWindow(); - if (focusedWindow == nullptr) + bool isCurrentUnityFocused = false; + + if (hwnd != nullptr) { - if (isUnityFocused) + WCHAR className[256]; + if (GetClassNameW(hwnd, className, 256) > 0) { - isUnityFocused = false; - if (unfocusCallback) unfocusCallback(); + isCurrentUnityFocused = (std::wstring(className).find(L"Unity") != std::wstring::npos); } - return; } - WCHAR className[256]; - GetClassName(focusedWindow, className, 256); - const std::wstring classStr(className); - - const bool isCurrentUnityFocused = (classStr.find(L"Unity") != std::wstring::npos); if (isCurrentUnityFocused && !isUnityFocused) { isUnityFocused = true; lastHeartbeat = std::chrono::steady_clock::now(); if (focusCallback) focusCallback(); - } else if (!isCurrentUnityFocused && isUnityFocused) {