From 6d1ff128a6f231389f6135336c8873450624b178 Mon Sep 17 00:00:00 2001 From: iThorgrim <125808072+iThorgrim@users.noreply.github.com> Date: Fri, 29 Aug 2025 14:53:09 +0200 Subject: [PATCH] feat(ElunaFileWatcher): Add file watcher and autoreload (#286) --- conf/mod_eluna.conf.dist | 16 +++ src/LuaEngine/ElunaFileWatcher.cpp | 216 +++++++++++++++++++++++++++++ src/LuaEngine/ElunaFileWatcher.h | 43 ++++++ src/LuaEngine/LuaEngine.cpp | 16 +++ src/LuaEngine/LuaEngine.h | 2 + 5 files changed, 293 insertions(+) create mode 100644 src/LuaEngine/ElunaFileWatcher.cpp create mode 100644 src/LuaEngine/ElunaFileWatcher.h diff --git a/conf/mod_eluna.conf.dist b/conf/mod_eluna.conf.dist index dea70a7..2047d02 100644 --- a/conf/mod_eluna.conf.dist +++ b/conf/mod_eluna.conf.dist @@ -38,6 +38,20 @@ # "/usr/local/lib/lua/%s/?.so;/usr/lib/x86_64-linux-gnu/lua/%s/?.so;/usr/local/lib/lua/%s/loadall.so;" # Default: "" # +# Eluna.AutoReload +# Description: Enable or disable automatic reloading of Lua scripts when files are modified. +# This feature watches the script directory for changes and automatically +# triggers a reload when .lua files are added, modified, or deleted. +# Useful for development but should be disabled in production environments. +# Default: false - (disabled) +# true - (enabled) +# +# Eluna.AutoReloadInterval +# Description: Sets the interval in seconds between file system checks for auto-reload. +# Lower values provide faster detection but use more CPU resources. +# Higher values reduce CPU usage but increase detection delay. +# Default: 1 - (check every 1 second) +# # Eluna.BytecodeCache # Description: Enable or disable bytecode caching for improved performance. # When enabled, Lua/MoonScript files are compiled to bytecode and cached in memory. @@ -52,6 +66,8 @@ Eluna.ScriptPath = "lua_scripts" Eluna.PlayerAnnounceReload = false Eluna.RequirePaths = "" Eluna.RequireCPaths = "" +Eluna.AutoReload = false +Eluna.AutoReloadInterval = 1 Eluna.BytecodeCache = true ################################################################################################### diff --git a/src/LuaEngine/ElunaFileWatcher.cpp b/src/LuaEngine/ElunaFileWatcher.cpp new file mode 100644 index 0000000..729f8ae --- /dev/null +++ b/src/LuaEngine/ElunaFileWatcher.cpp @@ -0,0 +1,216 @@ +/* +* Copyright (C) 2010 - 2016 Eluna Lua Engine +* This program is free software licensed under GPL version 3 +* Please see the included DOCS/LICENSE.md for more information +*/ + +#include "ElunaFileWatcher.h" +#include "LuaEngine.h" +#include "ElunaUtility.h" +#include + +ElunaFileWatcher::ElunaFileWatcher() : running(false), checkInterval(1) +{ +} + +ElunaFileWatcher::~ElunaFileWatcher() +{ + StopWatching(); +} + +void ElunaFileWatcher::StartWatching(const std::string& scriptPath, uint32 intervalSeconds) +{ + if (running.load()) + { + ELUNA_LOG_DEBUG("[ElunaFileWatcher]: Already watching files"); + return; + } + + if (scriptPath.empty()) + { + ELUNA_LOG_ERROR("[ElunaFileWatcher]: Cannot start watching - script path is empty"); + return; + } + + watchPath = scriptPath; + checkInterval = intervalSeconds; + running.store(true); + + ScanDirectory(watchPath); + + watcherThread = std::thread(&ElunaFileWatcher::WatchLoop, this); + + ELUNA_LOG_INFO("[ElunaFileWatcher]: Started watching '{}' (interval: {}s)", watchPath, checkInterval); +} + +void ElunaFileWatcher::StopWatching() +{ + if (!running.load()) + return; + + running.store(false); + + if (watcherThread.joinable()) + watcherThread.join(); + + fileTimestamps.clear(); + + ELUNA_LOG_INFO("[ElunaFileWatcher]: Stopped watching files"); +} + +void ElunaFileWatcher::WatchLoop() +{ + while (running.load()) + { + try + { + CheckForChanges(); + } + catch (const std::exception& e) + { + ELUNA_LOG_ERROR("[ElunaFileWatcher]: Error during file watching: {}", e.what()); + } + + std::this_thread::sleep_for(std::chrono::seconds(checkInterval)); + } +} + +bool ElunaFileWatcher::IsWatchedFileType(const std::string& filename) { + return (filename.length() >= 4 && filename.substr(filename.length() - 4) == ".lua") || + (filename.length() >= 4 && filename.substr(filename.length() - 4) == ".ext") || + (filename.length() >= 5 && filename.substr(filename.length() - 5) == ".moon"); +} + +void ElunaFileWatcher::ScanDirectory(const std::string& path) +{ + try + { + boost::filesystem::path dir(path); + + if (!boost::filesystem::exists(dir) || !boost::filesystem::is_directory(dir)) + return; + + boost::filesystem::directory_iterator end_iter; + + for (boost::filesystem::directory_iterator dir_iter(dir); dir_iter != end_iter; ++dir_iter) + { + std::string fullpath = dir_iter->path().generic_string(); + + if (boost::filesystem::is_directory(dir_iter->status())) + { + ScanDirectory(fullpath); + } + else if (boost::filesystem::is_regular_file(dir_iter->status())) + { + std::string filename = dir_iter->path().filename().generic_string(); + + if (IsWatchedFileType(filename)) + { + fileTimestamps[fullpath] = boost::filesystem::last_write_time(dir_iter->path()); + } + } + } + } + catch (const std::exception& e) + { + ELUNA_LOG_ERROR("[ElunaFileWatcher]: Error scanning directory '{}': {}", path, e.what()); + } +} + +void ElunaFileWatcher::CheckForChanges() +{ + bool hasChanges = false; + + try + { + boost::filesystem::path dir(watchPath); + + if (!boost::filesystem::exists(dir) || !boost::filesystem::is_directory(dir)) + return; + + boost::filesystem::directory_iterator end_iter; + + for (boost::filesystem::directory_iterator dir_iter(dir); dir_iter != end_iter; ++dir_iter) + { + if (ShouldReloadFile(dir_iter->path().generic_string())) + hasChanges = true; + } + + for (auto it = fileTimestamps.begin(); it != fileTimestamps.end();) + { + if (!boost::filesystem::exists(it->first)) + { + ELUNA_LOG_DEBUG("[ElunaFileWatcher]: File deleted: {}", it->first); + it = fileTimestamps.erase(it); + hasChanges = true; + } + else + { + ++it; + } + } + } + catch (const std::exception& e) + { + ELUNA_LOG_ERROR("[ElunaFileWatcher]: Error checking for changes: {}", e.what()); + return; + } + + if (hasChanges) + { + ELUNA_LOG_INFO("[ElunaFileWatcher]: Lua script changes detected - triggering reload"); + Eluna::ReloadEluna(); + + ScanDirectory(watchPath); + } +} + +bool ElunaFileWatcher::ShouldReloadFile(const std::string& filepath) +{ + try + { + boost::filesystem::path file(filepath); + + if (boost::filesystem::is_directory(file)) + { + boost::filesystem::directory_iterator end_iter; + + for (boost::filesystem::directory_iterator dir_iter(file); dir_iter != end_iter; ++dir_iter) + { + if (ShouldReloadFile(dir_iter->path().generic_string())) + return true; + } + return false; + } + + if (!boost::filesystem::is_regular_file(file)) + return false; + + std::string filename = file.filename().generic_string(); + + if (!IsWatchedFileType(filename)) return false; + + auto currentTime = boost::filesystem::last_write_time(file); + auto it = fileTimestamps.find(filepath); + + if (it == fileTimestamps.end()) + { + ELUNA_LOG_DEBUG("[ElunaFileWatcher]: New file detected: {}", filepath); + fileTimestamps[filepath] = currentTime; + return true; + } + + if (it->second != currentTime) + { + ELUNA_LOG_DEBUG("[ElunaFileWatcher]: File modified: {}", filepath); + it->second = currentTime; + return true; + } + } + catch (const std::exception& e) + { + ELUNA_LOG_ERROR("[ElunaFileWatcher]: Error checking file '{}': {}", filepath, e.what()); + } + + return false; +} diff --git a/src/LuaEngine/ElunaFileWatcher.h b/src/LuaEngine/ElunaFileWatcher.h new file mode 100644 index 0000000..fec29ba --- /dev/null +++ b/src/LuaEngine/ElunaFileWatcher.h @@ -0,0 +1,43 @@ +/* +* Copyright (C) 2010 - 2016 Eluna Lua Engine +* This program is free software licensed under GPL version 3 +* Please see the included DOCS/LICENSE.md for more information +*/ + +#ifndef ELUNA_FILE_WATCHER_H +#define ELUNA_FILE_WATCHER_H + +#include +#include +#include +#include +#include +#include +#include "Common.h" + +class ElunaFileWatcher +{ +public: + ElunaFileWatcher(); + ~ElunaFileWatcher(); + + void StartWatching(const std::string& scriptPath, uint32 intervalSeconds = 1); + void StopWatching(); + bool IsWatching() const { return running.load(); } + +private: + void WatchLoop(); + void ScanDirectory(const std::string& path); + void CheckForChanges(); + bool ShouldReloadFile(const std::string& filepath); + bool IsWatchedFileType(const std::string& filename); + + std::thread watcherThread; + std::atomic running; + std::string watchPath; + uint32 checkInterval; + + std::map fileTimestamps; +}; + +#endif diff --git a/src/LuaEngine/LuaEngine.cpp b/src/LuaEngine/LuaEngine.cpp index d78299f..7a659c0 100644 --- a/src/LuaEngine/LuaEngine.cpp +++ b/src/LuaEngine/LuaEngine.cpp @@ -50,6 +50,7 @@ Eluna* Eluna::GEluna = NULL; bool Eluna::reload = false; bool Eluna::initialized = false; Eluna::LockType Eluna::lock; +std::unique_ptr Eluna::fileWatcher; // Global bytecode cache that survives Eluna reloads static std::unordered_map globalBytecodeCache; @@ -75,6 +76,14 @@ void Eluna::Initialize() // Create global eluna GEluna = new Eluna(); + + // Start file watcher if enabled + if (eConfigMgr->GetOption("Eluna.AutoReload", false)) + { + uint32 watchInterval = eConfigMgr->GetOption("Eluna.AutoReloadInterval", 1); + fileWatcher = std::make_unique(); + fileWatcher->StartWatching(lua_folderpath, watchInterval); + } } void Eluna::Uninitialize() @@ -82,6 +91,13 @@ void Eluna::Uninitialize() LOCK_ELUNA; ASSERT(IsInitialized()); + // Stop file watcher + if (fileWatcher) + { + fileWatcher->StopWatching(); + fileWatcher.reset(); + } + delete GEluna; GEluna = NULL; diff --git a/src/LuaEngine/LuaEngine.h b/src/LuaEngine/LuaEngine.h index 1853b97..19be307 100644 --- a/src/LuaEngine/LuaEngine.h +++ b/src/LuaEngine/LuaEngine.h @@ -23,6 +23,7 @@ #include "HttpManager.h" #include "EventEmitter.h" #include "TicketMgr.h" +#include "ElunaFileWatcher.h" #include "LootMgr.h" #include #include @@ -120,6 +121,7 @@ private: static bool reload; static bool initialized; static LockType lock; + static std::unique_ptr fileWatcher; // Lua script locations static ScriptList lua_scripts;