// Файл: internal/plugin/plugin.go // Назначение: Система плагинов на основе Lua для расширения функциональности СУБД. // Позволяет загружать Lua-скрипты как плагины, выполнять их в изолированном окружении, // взаимодействовать с данными СУБД и логировать действия плагинов в общий лог-файл. package plugin import ( "fmt" "sync" "sync/atomic" "time" "os" "path/filepath" "strings" "futriis/internal/log" "futriis/internal/storage" lua "github.com/yuin/gopher-lua" ) // PluginStatus представляет состояние плагина type PluginStatus int32 const ( StatusLoaded PluginStatus = iota StatusRunning StatusStopped StatusError ) // Plugin представляет загруженный Lua-плагин type Plugin struct { Name string FilePath string Status atomic.Int32 LState *lua.LState logger *log.Logger storage *storage.Storage mu sync.RWMutex loadedAt time.Time version string author string description string } // PluginManager управляет всеми загруженными плагинами type PluginManager struct { plugins sync.Map // map[string]*Plugin logger *log.Logger storage *storage.Storage pluginsDir string eventBus chan PluginEvent enabled bool } // PluginEvent представляет событие от плагина type PluginEvent struct { PluginName string EventType string Data interface{} Timestamp int64 } // NewPluginManager создаёт новый менеджер плагинов func NewPluginManager(pluginsDir string, logger *log.Logger, store *storage.Storage, enabled bool) *PluginManager { pm := &PluginManager{ logger: logger, storage: store, pluginsDir: pluginsDir, eventBus: make(chan PluginEvent, 1000), enabled: enabled, } if !enabled { logger.Info("Plugin system is disabled") return pm } // Запускаем обработчик событий плагинов go pm.eventLoop() // Автоматически загружаем плагины из директории go pm.autoLoadPlugins() return pm } // autoLoadPlugins автоматически загружает все .lua файлы из директории плагинов func (pm *PluginManager) autoLoadPlugins() { if !pm.enabled { return } ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() for { <-ticker.C entries, err := os.ReadDir(pm.pluginsDir) if err != nil { if pm.logger != nil { pm.logger.Error(fmt.Sprintf("Failed to read plugins directory: %v", err)) } continue } for _, entry := range entries { if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".lua") { pluginName := strings.TrimSuffix(entry.Name(), ".lua") if _, exists := pm.plugins.Load(pluginName); !exists { pluginPath := filepath.Join(pm.pluginsDir, entry.Name()) if err := pm.LoadPlugin(pluginName, pluginPath); err != nil { if pm.logger != nil { pm.logger.Error(fmt.Sprintf("Failed to auto-load plugin %s: %v", pluginName, err)) } } } } } } } // LoadPlugin загружает Lua-плагин из файла func (pm *PluginManager) LoadPlugin(name, filePath string) error { if !pm.enabled { return fmt.Errorf("plugin system is disabled") } // Читаем файл плагина script, err := os.ReadFile(filePath) if err != nil { return fmt.Errorf("failed to read plugin file: %v", err) } // Создаём новое Lua-состояние L := lua.NewState() defer func() { if r := recover(); r != nil { L.Close() if pm.logger != nil { pm.logger.Error(fmt.Sprintf("Plugin %s panic: %v", name, r)) } } }() // Открываем стандартные библиотеки Lua lua.OpenBase(L) lua.OpenString(L) lua.OpenTable(L) lua.OpenMath(L) // Регистрируем функции СУБД для Lua pm.registerDatabaseFunctions(L) // Выполняем скрипт if err := L.DoString(string(script)); err != nil { L.Close() return fmt.Errorf("failed to execute plugin script: %v", err) } // Извлекаем метаданные плагина version := pm.getPluginMetadata(L, "version") author := pm.getPluginMetadata(L, "author") description := pm.getPluginMetadata(L, "description") // Создаём объект плагина plugin := &Plugin{ Name: name, FilePath: filePath, LState: L, logger: pm.logger, storage: pm.storage, loadedAt: time.Now(), version: version, author: author, description: description, } plugin.Status.Store(int32(StatusLoaded)) // Сохраняем плагин pm.plugins.Store(name, plugin) // Логируем загрузку if pm.logger != nil { pm.logger.Info(fmt.Sprintf("Plugin loaded: %s v%s by %s - %s", name, version, author, description)) } // Вызываем функцию инициализации плагина, если она есть if err := pm.callPluginFunction(plugin, "on_load"); err != nil { if pm.logger != nil { pm.logger.Warn(fmt.Sprintf("Plugin %s on_load error: %v", name, err)) } } return nil } // registerDatabaseFunctions регистрирует функции доступа к СУБД в Lua func (pm *PluginManager) registerDatabaseFunctions(L *lua.LState) { // Регистрируем функцию для получения базы данных L.SetGlobal("get_database", L.NewFunction(func(L *lua.LState) int { dbName := L.CheckString(1) db, err := pm.storage.GetDatabase(dbName) if err != nil { L.Push(lua.LNil) L.Push(lua.LString(err.Error())) return 2 } // Создаём пользовательский тип для базы данных ud := L.NewUserData() ud.Value = db L.SetMetatable(ud, L.GetTypeMetatable("database")) L.Push(ud) return 1 })) // Регистрируем функцию для получения коллекции L.SetGlobal("get_collection", L.NewFunction(func(L *lua.LState) int { dbName := L.CheckString(1) collName := L.CheckString(2) db, err := pm.storage.GetDatabase(dbName) if err != nil { L.Push(lua.LNil) L.Push(lua.LString(err.Error())) return 2 } coll, err := db.GetCollection(collName) if err != nil { L.Push(lua.LNil) L.Push(lua.LString(err.Error())) return 2 } ud := L.NewUserData() ud.Value = coll L.SetMetatable(ud, L.GetTypeMetatable("collection")) L.Push(ud) return 1 })) // Регистрируем функцию логирования для плагинов L.SetGlobal("plugin_log", L.NewFunction(func(L *lua.LState) int { level := L.CheckString(1) message := L.CheckString(2) if pm.logger != nil { logMsg := fmt.Sprintf("[PLUGIN] %s: %s", level, message) switch level { case "debug": pm.logger.Debug(logMsg) case "info": pm.logger.Info(logMsg) case "warn": pm.logger.Warn(logMsg) case "error": pm.logger.Error(logMsg) default: pm.logger.Info(logMsg) } } return 0 })) // Регистрируем функцию для отправки событий L.SetGlobal("emit_event", L.NewFunction(func(L *lua.LState) int { eventType := L.CheckString(1) eventData := L.CheckAny(2) // Получаем имя плагина из контекста (нужно передавать при вызове) event := PluginEvent{ EventType: eventType, Data: pm.luaValueToGo(eventData), Timestamp: time.Now().UnixMilli(), } select { case pm.eventBus <- event: default: if pm.logger != nil { pm.logger.Warn("Plugin event bus full, event dropped") } } return 0 })) // Устанавливаем метатаблицы для методов баз данных и коллекций pm.setupDatabaseMetatable(L) pm.setupCollectionMetatable(L) } // setupDatabaseMetatable настраивает методы для объекта базы данных в Lua func (pm *PluginManager) setupDatabaseMetatable(L *lua.LState) { mt := L.NewTypeMetatable("database") L.SetField(mt, "__index", L.NewFunction(func(L *lua.LState) int { db := L.CheckUserData(1).Value.(*storage.Database) method := L.CheckString(2) switch method { case "create_collection": L.Push(L.NewFunction(func(L *lua.LState) int { name := L.CheckString(1) err := db.CreateCollection(name) if err != nil { L.Push(lua.LString(err.Error())) return 1 } L.Push(lua.LNil) return 1 })) case "get_collection": L.Push(L.NewFunction(func(L *lua.LState) int { name := L.CheckString(1) coll, err := db.GetCollection(name) if err != nil { L.Push(lua.LNil) L.Push(lua.LString(err.Error())) return 2 } ud := L.NewUserData() ud.Value = coll L.SetMetatable(ud, L.GetTypeMetatable("collection")) L.Push(ud) return 1 })) case "name": L.Push(lua.LString(db.Name())) default: L.Push(lua.LNil) } return 1 })) } // setupCollectionMetatable настраивает методы для объекта коллекции в Lua func (pm *PluginManager) setupCollectionMetatable(L *lua.LState) { mt := L.NewTypeMetatable("collection") L.SetField(mt, "__index", L.NewFunction(func(L *lua.LState) int { coll := L.CheckUserData(1).Value.(*storage.Collection) method := L.CheckString(2) switch method { case "insert": L.Push(L.NewFunction(func(L *lua.LState) int { doc := L.CheckTable(1) // Конвертируем Lua table в map fields := make(map[string]interface{}) doc.ForEach(func(key, value lua.LValue) { if key.Type() == lua.LTString { fields[key.String()] = pm.luaValueToGo(value) } }) // Вставляем документ err := coll.InsertFromMap(fields) if err != nil { L.Push(lua.LString(err.Error())) return 1 } L.Push(lua.LNil) return 1 })) case "find": L.Push(L.NewFunction(func(L *lua.LState) int { id := L.CheckString(1) doc, err := coll.Find(id) if err != nil { L.Push(lua.LNil) L.Push(lua.LString(err.Error())) return 2 } // Конвертируем документ в Lua table table := L.NewTable() for k, v := range doc.GetFields() { table.RawSetString(k, pm.goValueToLua(L, v)) } L.Push(table) return 1 })) case "update": L.Push(L.NewFunction(func(L *lua.LState) int { id := L.CheckString(1) updates := L.CheckTable(2) fields := make(map[string]interface{}) updates.ForEach(func(key, value lua.LValue) { if key.Type() == lua.LTString { fields[key.String()] = pm.luaValueToGo(value) } }) err := coll.Update(id, fields) if err != nil { L.Push(lua.LString(err.Error())) return 1 } L.Push(lua.LNil) return 1 })) case "delete": L.Push(L.NewFunction(func(L *lua.LState) int { id := L.CheckString(1) err := coll.Delete(id) if err != nil { L.Push(lua.LString(err.Error())) return 1 } L.Push(lua.LNil) return 1 })) case "count": L.Push(L.NewFunction(func(L *lua.LState) int { count := coll.Count() L.Push(lua.LNumber(count)) return 1 })) default: L.Push(lua.LNil) } return 1 })) } // luaValueToGo конвертирует Lua-значение в Go-значение func (pm *PluginManager) luaValueToGo(val lua.LValue) interface{} { if val == nil || val == lua.LNil { return nil } switch v := val.(type) { case lua.LString: return string(v) case lua.LNumber: return float64(v) case lua.LBool: return bool(v) case *lua.LTable: result := make(map[string]interface{}) v.ForEach(func(key, value lua.LValue) { keyStr := "unknown" if key.Type() == lua.LTString { keyStr = key.String() } else if key.Type() == lua.LTNumber { keyStr = fmt.Sprintf("%d", int64(key.(lua.LNumber))) } result[keyStr] = pm.luaValueToGo(value) }) return result default: return v.String() } } // goValueToLua конвертирует Go-значение в Lua-значение func (pm *PluginManager) goValueToLua(L *lua.LState, val interface{}) lua.LValue { if val == nil { return lua.LNil } switch v := val.(type) { case string: return lua.LString(v) case int: return lua.LNumber(float64(v)) case int64: return lua.LNumber(float64(v)) case float32: return lua.LNumber(float64(v)) case float64: return lua.LNumber(v) case bool: return lua.LBool(v) case map[string]interface{}: table := L.NewTable() for k, val := range v { table.RawSetString(k, pm.goValueToLua(L, val)) } return table case []interface{}: table := L.NewTable() for i, val := range v { table.RawSetInt(i+1, pm.goValueToLua(L, val)) } return table default: return lua.LString(fmt.Sprintf("%v", v)) } } // getPluginMetadata извлекает метаданные из загруженного Lua-скрипта func (pm *PluginManager) getPluginMetadata(L *lua.LState, field string) string { // Пытаемся получить глобальную переменную с метаданными val := L.GetGlobal(field) if str, ok := val.(lua.LString); ok { return string(str) } return "unknown" } // callPluginFunction вызывает функцию плагина по имени func (pm *PluginManager) callPluginFunction(plugin *Plugin, funcName string) error { plugin.mu.RLock() defer plugin.mu.RUnlock() L := plugin.LState fn := L.GetGlobal(funcName) if fn == lua.LNil { return nil // Функция не определена } if err := L.CallByParam(lua.P{ Fn: fn, NRet: 0, Protect: true, }); err != nil { return fmt.Errorf("failed to call %s: %v", funcName, err) } return nil } // eventLoop обрабатывает события от плагинов func (pm *PluginManager) eventLoop() { for event := range pm.eventBus { if pm.logger != nil { pm.logger.Debug(fmt.Sprintf("Plugin event [%s]: %+v", event.EventType, event.Data)) } // Можно реализовать подписку плагинов на события pm.plugins.Range(func(key, value interface{}) bool { plugin := value.(*Plugin) // Асинхронно уведомляем плагины о событии go pm.notifyPlugin(plugin, event) return true }) } } // notifyPlugin уведомляет конкретный плагин о событии func (pm *PluginManager) notifyPlugin(plugin *Plugin, event PluginEvent) { plugin.mu.RLock() defer plugin.mu.RUnlock() L := plugin.LState if L == nil { return } fn := L.GetGlobal("on_event") if fn == lua.LNil { return } // Устанавливаем имя плагина в событие event.PluginName = plugin.Name // Создаём таблицу с данными события eventTable := L.NewTable() eventTable.RawSetString("type", lua.LString(event.EventType)) eventTable.RawSetString("plugin_name", lua.LString(event.PluginName)) eventTable.RawSetString("timestamp", lua.LNumber(event.Timestamp)) eventTable.RawSetString("data", pm.goValueToLua(L, event.Data)) L.SetGlobal("event", eventTable) if err := L.CallByParam(lua.P{ Fn: fn, NRet: 0, Protect: true, }); err != nil { if pm.logger != nil { pm.logger.Error(fmt.Sprintf("Plugin %s on_event error: %v", plugin.Name, err)) } } } // ExecutePlugin выполняет пользовательскую функцию плагина func (pm *PluginManager) ExecutePlugin(pluginName, funcName string, args ...interface{}) (interface{}, error) { if !pm.enabled { return nil, fmt.Errorf("plugin system is disabled") } val, ok := pm.plugins.Load(pluginName) if !ok { return nil, fmt.Errorf("plugin not found: %s", pluginName) } plugin := val.(*Plugin) if PluginStatus(plugin.Status.Load()) != StatusRunning { return nil, fmt.Errorf("plugin %s is not running", pluginName) } plugin.mu.RLock() defer plugin.mu.RUnlock() L := plugin.LState if L == nil { return nil, fmt.Errorf("plugin %s has no Lua state", pluginName) } fn := L.GetGlobal(funcName) if fn == lua.LNil { return nil, fmt.Errorf("function %s not found in plugin %s", funcName, pluginName) } // Подготавливаем аргументы для вызова luaArgs := make([]lua.LValue, len(args)) for i, arg := range args { luaArgs[i] = pm.goValueToLua(L, arg) } // Вызываем функцию if err := L.CallByParam(lua.P{ Fn: fn, NRet: 1, Protect: true, }, luaArgs...); err != nil { return nil, fmt.Errorf("plugin execution failed: %v", err) } ret := L.Get(-1) L.Pop(1) return pm.luaValueToGo(ret), nil } // UnloadPlugin выгружает плагин func (pm *PluginManager) UnloadPlugin(name string) error { if !pm.enabled { return fmt.Errorf("plugin system is disabled") } val, ok := pm.plugins.Load(name) if !ok { return fmt.Errorf("plugin not found: %s", name) } plugin := val.(*Plugin) // Вызываем функцию выгрузки if err := pm.callPluginFunction(plugin, "on_unload"); err != nil { if pm.logger != nil { pm.logger.Warn(fmt.Sprintf("Plugin %s on_unload error: %v", name, err)) } } // Закрываем Lua-состояние if plugin.LState != nil { plugin.LState.Close() } plugin.Status.Store(int32(StatusStopped)) pm.plugins.Delete(name) if pm.logger != nil { pm.logger.Info(fmt.Sprintf("Plugin unloaded: %s", name)) } return nil } // StartPlugin запускает плагин func (pm *PluginManager) StartPlugin(name string) error { if !pm.enabled { return fmt.Errorf("plugin system is disabled") } val, ok := pm.plugins.Load(name) if !ok { return fmt.Errorf("plugin not found: %s", name) } plugin := val.(*Plugin) plugin.Status.Store(int32(StatusRunning)) if err := pm.callPluginFunction(plugin, "on_start"); err != nil { plugin.Status.Store(int32(StatusError)) return fmt.Errorf("failed to start plugin: %v", err) } if pm.logger != nil { pm.logger.Info(fmt.Sprintf("Plugin started: %s", name)) } return nil } // StopPlugin останавливает плагин func (pm *PluginManager) StopPlugin(name string) error { if !pm.enabled { return fmt.Errorf("plugin system is disabled") } val, ok := pm.plugins.Load(name) if !ok { return fmt.Errorf("plugin not found: %s", name) } plugin := val.(*Plugin) if err := pm.callPluginFunction(plugin, "on_stop"); err != nil { if pm.logger != nil { pm.logger.Warn(fmt.Sprintf("Plugin %s on_stop error: %v", name, err)) } } plugin.Status.Store(int32(StatusStopped)) if pm.logger != nil { pm.logger.Info(fmt.Sprintf("Plugin stopped: %s", name)) } return nil } // ListPlugins возвращает список всех загруженных плагинов func (pm *PluginManager) ListPlugins() []*Plugin { plugins := make([]*Plugin, 0) pm.plugins.Range(func(key, value interface{}) bool { plugins = append(plugins, value.(*Plugin)) return true }) return plugins } // IsEnabled возвращает статус системы плагинов func (pm *PluginManager) IsEnabled() bool { return pm.enabled }