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
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ set(SOURCES
src/file_watcher.cpp
src/wakatime_client.cpp
src/tray_icon.cpp
src/windows_dark_mode.cpp
src/unity_focus_detector.cpp
)

Expand All @@ -21,6 +22,7 @@ set(HEADERS
include/file_watcher.h
include/wakatime_client.h
include/tray_icon.h
include/windows_dark_mode.h
include/unity_focus_detector.h
)

Expand Down Expand Up @@ -52,6 +54,7 @@ if(WIN32)
ole32
oleaut32
windowscodecs
dwmapi
user32
)
else()
Expand All @@ -64,6 +67,7 @@ if(WIN32)
ole32
oleaut32
windowscodecs
dwmapi
user32
)
endif()
Expand Down
21 changes: 19 additions & 2 deletions include/file_watcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,33 @@ class FileWatcher {
OVERLAPPED overlapped; // 비동기 I/O용 구조체
char buffer[4096]; // 변경 정보를 받을 버퍼
HANDLE stopEvent;
HANDLE ioEvent;

WatchedProject() : shouldStop(false) {
WatchedProject() :
directoryHandle(INVALID_HANDLE_VALUE),
shouldStop(false),
stopEvent(nullptr),
ioEvent(nullptr)
{
stopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
ioEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
ZeroMemory(&overlapped, sizeof(OVERLAPPED));
overlapped.hEvent = ioEvent;
ZeroMemory(buffer, sizeof(buffer));
}

~WatchedProject() {
if (stopEvent != INVALID_HANDLE_VALUE) {
if (stopEvent != nullptr) {
CloseHandle(stopEvent);
stopEvent = nullptr;
}
if (ioEvent != nullptr) {
CloseHandle(ioEvent);
ioEvent = nullptr;
}
if (directoryHandle != nullptr && directoryHandle != INVALID_HANDLE_VALUE) {
CloseHandle(directoryHandle);
directoryHandle = INVALID_HANDLE_VALUE;
}
}
};
Expand Down
6 changes: 3 additions & 3 deletions include/unity_focus_detector.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class UnityFocusDetector {
private:
bool isUnityFocused = false;
std::chrono::steady_clock::time_point lastHeartbeat;
std::chrono::seconds heartbeatInterval{120}; // 5분
std::chrono::seconds heartbeatInterval{120}; // 2분

std::function<void()> focusCallback;
std::function<void()> unfocusCallback;
Expand All @@ -18,7 +18,7 @@ class UnityFocusDetector {
void CheckFocused();

/**
* 5분마다 호출
* 2분마다 호출
*/
void SendPeriodicHeartbeat();

Expand All @@ -41,4 +41,4 @@ class UnityFocusDetector {
* @param callback 이전 하트비트 보낼때 호출될 함수
*/
void SetPeriodicHeartbeatCallback(std::function<void()> callback);
};
};
18 changes: 18 additions & 0 deletions include/windows_dark_mode.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#pragma once

#include <windows.h>

namespace WindowsDarkMode
{
/**
* Enable dark-mode preference for Win32 menus on supported Windows versions.
* Returns true when the API path is available and invoked.
*/
bool EnableForApp();

/**
* Apply dark-mode attributes to a specific window when supported.
* Safe to call on unsupported versions (no-op fallback).
*/
void ApplyToWindow(HWND hwnd);
}
4 changes: 4 additions & 0 deletions main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "wakatime_client.h"
#include "tray_icon.h"
#include "unity_focus_detector.h"
#include "windows_dark_mode.h"

WakaTimeClient *g_wakatimeClient = nullptr;
FileWatcher *g_fileWatcher = nullptr;
Expand Down Expand Up @@ -243,6 +244,9 @@ void InitialUnityProjectScan()
int main()
{
std::cout << "[Main] Unity WakaTime Monitor Starting..." << std::endl;
const bool darkModeAvailable = WindowsDarkMode::EnableForApp();
std::cout << "[Main] Dark mode menu opt-in: "
<< (darkModeAvailable ? "enabled" : "not available") << std::endl;

TrayIcon trayIcon;
g_trayIcon = &trayIcon;
Expand Down
158 changes: 95 additions & 63 deletions src/file_watcher.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "file_watcher.h"
#include <utility>
#include <system_error>

FileWatcher::FileWatcher()
{
Expand Down Expand Up @@ -37,13 +38,20 @@ bool FileWatcher::StartWatching(const std::string &projectPath, const std::strin

// 새로운 WatchedProject 생성
auto project = std::make_unique<WatchedProject>();
if (project->stopEvent == nullptr || project->ioEvent == nullptr)
{
std::cerr << "[FileWatcher] Failed to create watcher events for: " << projectPath << std::endl;
return false;
}

project->projectPath = projectPath;
project->projectName = projectName;
project->unityVersion = unityVersion;
project->shouldStop = false;

// ZeroMemory: 메모리를 0으로 초기화 (Windows API 함수)
ZeroMemory(&project->overlapped, sizeof(OVERLAPPED));
project->overlapped.hEvent = project->ioEvent;
ZeroMemory(project->buffer, sizeof(project->buffer));

// CreateFile: 디렉토리 핸들 열기
Expand Down Expand Up @@ -72,19 +80,41 @@ bool FileWatcher::StartWatching(const std::string &projectPath, const std::strin

// 감시 스레드 시작
WatchedProject *projectPtr = project.get();
project->watchThread = std::thread(&FileWatcher::WatchProjectThread, this, projectPtr);
try
{
project->watchThread = std::thread(&FileWatcher::WatchProjectThread, this, projectPtr);
}
catch (const std::system_error &e)
{
std::cerr << "[FileWatcher] Failed to start watch thread for " << projectName << ": " << e.what() << std::endl;
return false;
}

watchedProjects.push_back(std::move(project));

return true;
}

void FileWatcher::WatchProjectThread(WatchedProject *project)
{
if (project == nullptr ||
project->directoryHandle == nullptr ||
project->directoryHandle == INVALID_HANDLE_VALUE ||
project->stopEvent == nullptr ||
project->ioEvent == nullptr)
{
std::cerr << "[FileWatcher] Invalid watch project state, thread exit" << std::endl;
return;
}

std::cout << "[FileWatcher] Watch thread started for: " << project->projectName << std::endl;

while (!project->shouldStop)
{
DWORD bytesReturned = 0;
ZeroMemory(&project->overlapped, sizeof(OVERLAPPED));
project->overlapped.hEvent = project->ioEvent;
ResetEvent(project->ioEvent);

// ReadDirectoryChangesW: 디렉토리 변경사항을 감지하는 핵심 함수
// TRUE: 하위 폴더도 감시
Expand All @@ -107,9 +137,18 @@ void FileWatcher::WatchProjectThread(WatchedProject *project)
break;
}
}
else
{
// 드물게 동기 완료될 수 있으므로 즉시 처리
if (bytesReturned > 0)
{
ProcessFileChanges(project->buffer, bytesReturned, project);
}
continue;
}

const HANDLE waitHandles[2] = {
project->directoryHandle, // I/O 완료 대기
project->ioEvent, // I/O 완료 대기
project->stopEvent // 종료 신호 대기
};

Expand All @@ -123,7 +162,7 @@ void FileWatcher::WatchProjectThread(WatchedProject *project)

switch (waitResult)
{
case WAIT_OBJECT_0: // directoryHandle 신호 (I/O 완료)
case WAIT_OBJECT_0: // ioEvent 신호 (I/O 완료)
{
// GetOverlappedResult로 결과 확인
if (GetOverlappedResult(project->directoryHandle, &project->overlapped, &bytesReturned, FALSE))
Expand All @@ -133,11 +172,14 @@ void FileWatcher::WatchProjectThread(WatchedProject *project)
}
else
{
if (const DWORD error = GetLastError(); error != ERROR_OPERATION_ABORTED)
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;
}
break;
if (project->shouldStop)
{
goto thread_exit;
}
}
break;
}
Expand All @@ -158,8 +200,6 @@ void FileWatcher::WatchProjectThread(WatchedProject *project)
std::cerr << "[FileWatcher] WaitForMultipleObjects failed for " << project->projectName << " (Error: " << GetLastError() << ")" << std::endl;
goto thread_exit;
}

ZeroMemory(&project->overlapped, sizeof(OVERLAPPED));
}

thread_exit:
Expand Down Expand Up @@ -260,84 +300,76 @@ bool FileWatcher::ShouldIgnoreFolder(const std::string &folderName) const

void FileWatcher::StopWatching(const std::string &projectPath)
{
std::lock_guard<std::mutex> lock(projectsMutex);
std::unique_ptr<WatchedProject> projectToStop;

{
std::lock_guard<std::mutex> lock(projectsMutex);

const auto it = std::find_if(watchedProjects.begin(), watchedProjects.end(),
[&projectPath](const std::unique_ptr<WatchedProject> &project)
{
return project->projectPath == projectPath;
});

const auto it = std::remove_if(watchedProjects.begin(), watchedProjects.end(),
[&projectPath](const std::unique_ptr<WatchedProject> &project)
{
if (project->projectPath == projectPath)
{
std::cout << "[FileWatcher] Stopping watch for: " << project->projectName << std::endl;
if (it == watchedProjects.end())
{
return;
}

project->shouldStop = true;
projectToStop = std::move(*it);
watchedProjects.erase(it);
}

SetEvent(project->stopEvent);
CancelIo(project->directoryHandle); // I/O 작업 취소
std::cout << "[FileWatcher] Stopping watch for: " << projectToStop->projectName << std::endl;
projectToStop->shouldStop = true;

// 스레드 종료 대기
if (project->watchThread.joinable())
{
const auto future = std::async(std::launch::async, [&project]()
{
project->watchThread.join();
});

// 5초 타임아웃으로 join 대기
if (future.wait_for(std::chrono::seconds(5)) == std::future_status::timeout)
{
std::cout << "[FileWatcher] Thread join timeout, forcing termination" << std::endl;
project->watchThread.detach(); // 강제 종료
}
}

CloseHandle(project->directoryHandle);
return true;
}
return false;
});

watchedProjects.erase(it, watchedProjects.end());
if (projectToStop->stopEvent != nullptr)
{
SetEvent(projectToStop->stopEvent);
}
if (projectToStop->directoryHandle != nullptr && projectToStop->directoryHandle != INVALID_HANDLE_VALUE)
{
CancelIoEx(projectToStop->directoryHandle, nullptr);
}
if (projectToStop->watchThread.joinable())
{
projectToStop->watchThread.join();
}
}

void FileWatcher::StopAllWatching()
{
std::lock_guard<std::mutex> lock(projectsMutex);
std::vector<std::unique_ptr<WatchedProject>> projectsToStop;

{
std::lock_guard<std::mutex> lock(projectsMutex);
projectsToStop.swap(watchedProjects);
}

std::cout << "[FileWatcher] Stopping all watches..." << std::endl;

for (const auto &project: watchedProjects)
for (const auto &project: projectsToStop)
{
project->shouldStop = true;
SetEvent(project->stopEvent);
CancelIo(project->directoryHandle);
if (project->stopEvent != nullptr)
{
SetEvent(project->stopEvent);
}
if (project->directoryHandle != nullptr && project->directoryHandle != INVALID_HANDLE_VALUE)
{
CancelIoEx(project->directoryHandle, nullptr);
}
}

// 모든 스레드 종료 대기
int joinedCount = 0;
for (auto &project: watchedProjects)
for (auto &project: projectsToStop)
{
if (project->watchThread.joinable())
{
auto future = std::async(std::launch::async, [&project]()
{
project->watchThread.join();
});

if (future.wait_for(std::chrono::seconds(3)) == std::future_status::ready)
{
joinedCount++;
}
else
{
std::cout << "[FileWatcher] Thread join timeout: " << project->projectName << std::endl;
project->watchThread.detach();
}
project->watchThread.join();
}

CloseHandle(project->directoryHandle);
}

watchedProjects.clear();
std::cout << "[FileWatcher] All watches stopped" << std::endl;
}

Expand Down
Loading