diff --git a/internal/plugin/old-plugin.go b/internal/plugin/old-plugin.go deleted file mode 100644 index 0d9a5c1..0000000 --- a/internal/plugin/old-plugin.go +++ /dev/null @@ -1,797 +0,0 @@ -/* - * Copyright 2026 Safronov Grigorii - * - * Licensed under the CDDL, Version 1.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * You may obtain a copy of the License at - * https://opensource.org/licenses/CDDL-1.0 - */ - -// Файл: 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 { - if logger != nil { - 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) - - // Выполняем скрипт с защитой от паники - var execErr error - func() { - defer func() { - if r := recover(); r != nil { - execErr = fmt.Errorf("panic during script execution: %v", r) - } - }() - execErr = L.DoString(string(script)) - }() - - if execErr != nil { - L.Close() - return fmt.Errorf("failed to execute plugin script: %v", execErr) - } - - // Извлекаем метаданные плагина - 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) - } - }) - - // Вставляем документ - newDoc := storage.NewDocument() - for k, v := range fields { - newDoc.SetField(k, v) - } - - err := coll.Insert(newDoc) - if err != nil { - L.Push(lua.LNil) - L.Push(lua.LString(err.Error())) - return 2 - } - // Возвращаем ID вставленного документа - L.Push(lua.LString(newDoc.ID)) - L.Push(lua.LNil) - return 2 - })) - 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() - table.RawSetString("_id", lua.LString(doc.ID)) - 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 - if L == nil { - return fmt.Errorf("plugin has no Lua state") - } - - fn := L.GetGlobal(funcName) - if fn == lua.LNil { - return nil // Функция не определена - } - - // Защита от паники при вызове Lua функции - var callErr error - func() { - defer func() { - if r := recover(); r != nil { - callErr = fmt.Errorf("panic during %s: %v", funcName, r) - } - }() - callErr = L.CallByParam(lua.P{ - Fn: fn, - NRet: 0, - Protect: true, - }) - }() - - if callErr != nil { - return fmt.Errorf("failed to call %s: %v", funcName, callErr) - } - - 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)) - - // Не устанавливаем глобальную переменную, передаём как аргумент - var callErr error - func() { - defer func() { - if r := recover(); r != nil { - callErr = fmt.Errorf("panic during on_event: %v", r) - } - }() - callErr = L.CallByParam(lua.P{ - Fn: fn, - NRet: 0, - Protect: true, - }, eventTable) - }() - - if callErr != nil && pm.logger != nil { - pm.logger.Error(fmt.Sprintf("Plugin %s on_event error: %v", plugin.Name, callErr)) - } -} - -// 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) - } - - // Вызываем функцию с защитой от паники - var ret lua.LValue - var callErr error - func() { - defer func() { - if r := recover(); r != nil { - callErr = fmt.Errorf("panic during execution: %v", r) - } - }() - callErr = L.CallByParam(lua.P{ - Fn: fn, - NRet: 1, - Protect: true, - }, luaArgs...) - if callErr == nil { - ret = L.Get(-1) - L.Pop(1) - } - }() - - if callErr != nil { - return nil, fmt.Errorf("plugin execution failed: %v", callErr) - } - - 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 -}