#pragma once #include #include #include #include #ifdef _WIN32 #include // 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 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