feat(ElunaFileWatcher): Add file watcher and autoreload (#286)

This commit is contained in:
iThorgrim
2025-08-29 14:53:09 +02:00
committed by GitHub
parent 65af80f08d
commit 6d1ff128a6
5 changed files with 293 additions and 0 deletions

View File

@@ -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
###################################################################################################

View File

@@ -0,0 +1,216 @@
/*
* Copyright (C) 2010 - 2016 Eluna Lua Engine <http://emudevs.com/>
* 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 <boost/filesystem.hpp>
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;
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) 2010 - 2016 Eluna Lua Engine <http://emudevs.com/>
* 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 <thread>
#include <atomic>
#include <map>
#include <string>
#include <chrono>
#include <boost/filesystem.hpp>
#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<bool> running;
std::string watchPath;
uint32 checkInterval;
std::map<std::string, std::time_t> fileTimestamps;
};
#endif

View File

@@ -50,6 +50,7 @@ Eluna* Eluna::GEluna = NULL;
bool Eluna::reload = false;
bool Eluna::initialized = false;
Eluna::LockType Eluna::lock;
std::unique_ptr<ElunaFileWatcher> Eluna::fileWatcher;
// Global bytecode cache that survives Eluna reloads
static std::unordered_map<std::string, GlobalCacheEntry> globalBytecodeCache;
@@ -75,6 +76,14 @@ void Eluna::Initialize()
// Create global eluna
GEluna = new Eluna();
// Start file watcher if enabled
if (eConfigMgr->GetOption<bool>("Eluna.AutoReload", false))
{
uint32 watchInterval = eConfigMgr->GetOption<uint32>("Eluna.AutoReloadInterval", 1);
fileWatcher = std::make_unique<ElunaFileWatcher>();
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;

View File

@@ -23,6 +23,7 @@
#include "HttpManager.h"
#include "EventEmitter.h"
#include "TicketMgr.h"
#include "ElunaFileWatcher.h"
#include "LootMgr.h"
#include <mutex>
#include <memory>
@@ -120,6 +121,7 @@ private:
static bool reload;
static bool initialized;
static LockType lock;
static std::unique_ptr<ElunaFileWatcher> fileWatcher;
// Lua script locations
static ScriptList lua_scripts;