- Port to Windows (MinGW/Ninja): - ModuleFactory/ModuleLoader: LoadLibrary/GetProcAddress - SystemUtils: Windows process memory APIs - FileWatcher: st_mtime instead of st_mtim - IIO.h: add missing #include <cstdint> - Tests (09, 10, 11): grove_dlopen/dlsym wrappers - Phase 4 - SceneCollector & IIO: - Implement view/proj matrix calculation in parseCamera() - Add IIO routing test with game→renderer pattern - test_22_bgfx_sprites_headless: 5 tests, 23 assertions pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
129 lines
3.3 KiB
C++
129 lines
3.3 KiB
C++
#pragma once
|
|
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <sys/stat.h>
|
|
#include <chrono>
|
|
|
|
#ifdef _WIN32
|
|
#include <ctime>
|
|
// Windows doesn't have timespec in all cases, and uses st_mtime instead of st_mtim
|
|
struct FileTimeInfo {
|
|
time_t tv_sec;
|
|
long tv_nsec;
|
|
};
|
|
#endif
|
|
|
|
namespace grove {
|
|
|
|
/**
|
|
* @brief Simple file modification watcher
|
|
*
|
|
* Watches files for modifications by checking their mtime.
|
|
* Efficient for hot-reload scenarios where we check a few specific files.
|
|
*/
|
|
class FileWatcher {
|
|
private:
|
|
#ifdef _WIN32
|
|
using TimeSpec = FileTimeInfo;
|
|
#else
|
|
using TimeSpec = timespec;
|
|
#endif
|
|
|
|
struct FileInfo {
|
|
TimeSpec lastModified;
|
|
bool exists;
|
|
};
|
|
|
|
std::unordered_map<std::string, FileInfo> watchedFiles;
|
|
|
|
TimeSpec getModificationTime(const std::string& path) {
|
|
struct stat fileStat;
|
|
if (stat(path.c_str(), &fileStat) == 0) {
|
|
#ifdef _WIN32
|
|
// Windows uses st_mtime (seconds only)
|
|
return {fileStat.st_mtime, 0};
|
|
#else
|
|
return fileStat.st_mtim;
|
|
#endif
|
|
}
|
|
return {0, 0};
|
|
}
|
|
|
|
bool timesEqual(const TimeSpec& a, const TimeSpec& b) {
|
|
return a.tv_sec == b.tv_sec && a.tv_nsec == b.tv_nsec;
|
|
}
|
|
|
|
public:
|
|
/**
|
|
* @brief Start watching a file
|
|
* @param path Path to file to watch
|
|
*/
|
|
void watch(const std::string& path) {
|
|
FileInfo info;
|
|
info.lastModified = getModificationTime(path);
|
|
info.exists = (info.lastModified.tv_sec != 0 || info.lastModified.tv_nsec != 0);
|
|
watchedFiles[path] = info;
|
|
}
|
|
|
|
/**
|
|
* @brief Check if a file has been modified since last check
|
|
* @param path Path to file to check
|
|
* @return True if file was modified
|
|
*/
|
|
bool hasChanged(const std::string& path) {
|
|
auto it = watchedFiles.find(path);
|
|
if (it == watchedFiles.end()) {
|
|
// Not watching this file yet
|
|
return false;
|
|
}
|
|
|
|
FileInfo& oldInfo = it->second;
|
|
TimeSpec currentMod = getModificationTime(path);
|
|
bool currentExists = (currentMod.tv_sec != 0 || currentMod.tv_nsec != 0);
|
|
|
|
// Check if existence changed
|
|
if (oldInfo.exists != currentExists) {
|
|
oldInfo.lastModified = currentMod;
|
|
oldInfo.exists = currentExists;
|
|
return true;
|
|
}
|
|
|
|
// Check if modification time changed
|
|
if (!timesEqual(oldInfo.lastModified, currentMod)) {
|
|
oldInfo.lastModified = currentMod;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @brief Reset a file's tracked state (useful after processing change)
|
|
* @param path Path to file to reset
|
|
*/
|
|
void reset(const std::string& path) {
|
|
auto it = watchedFiles.find(path);
|
|
if (it != watchedFiles.end()) {
|
|
it->second.lastModified = getModificationTime(path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Stop watching a file
|
|
* @param path Path to file to stop watching
|
|
*/
|
|
void unwatch(const std::string& path) {
|
|
watchedFiles.erase(path);
|
|
}
|
|
|
|
/**
|
|
* @brief Stop watching all files
|
|
*/
|
|
void unwatchAll() {
|
|
watchedFiles.clear();
|
|
}
|
|
};
|
|
|
|
} // namespace grove
|