diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go new file mode 100644 index 0000000..d8cf8d1 --- /dev/null +++ b/internal/plugin/plugin.go @@ -0,0 +1,1067 @@ +/* + * 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 + } + + // Создаём директорию для плагинов, если её нет + if err := os.MkdirAll(pluginsDir, 0755); err != nil { + if logger != nil { + logger.Error(fmt.Sprintf("Failed to create plugins directory: %v", err)) + } + } + + // Запускаем обработчик событий плагинов + go pm.eventLoop() + + // Автоматически загружаем плагины из директории + go pm.autoLoadPlugins() + + if logger != nil { + logger.Info(fmt.Sprintf("Plugin system initialized, plugins directory: %s", pluginsDir)) + } + + return pm +} + +// autoLoadPlugins автоматически загружает все .lua файлы из директории плагинов +func (pm *PluginManager) autoLoadPlugins() { + if !pm.enabled { + return + } + + // Первоначальная загрузка + pm.loadPluginsFromDir() + + // Периодическая проверка новых плагинов (каждые 10 секунд) + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for range ticker.C { + pm.loadPluginsFromDir() + } +} + +// loadPluginsFromDir загружает все плагины из директории +func (pm *PluginManager) loadPluginsFromDir() { + 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)) + } + return + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + name := entry.Name() + if !strings.HasSuffix(name, ".lua") { + continue + } + + pluginName := strings.TrimSuffix(name, ".lua") + if _, exists := pm.plugins.Load(pluginName); !exists { + pluginPath := filepath.Join(pm.pluginsDir, 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)) + } + } else if pm.logger != nil { + pm.logger.Info(fmt.Sprintf("Auto-loaded plugin: %s", pluginName)) + } + } + } +} + +// 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) + + // Регистрируем функции транзакций + pm.registerTransactionFunctions(L) + + // Регистрируем функции триггеров + pm.registerTriggerFunctions(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) +} + +// registerTransactionFunctions регистрирует функции для работы с транзакциями +func (pm *PluginManager) registerTransactionFunctions(L *lua.LState) { + L.SetGlobal("begin_transaction", L.NewFunction(func(L *lua.LState) int { + tx := storage.BeginTransaction() + if tx == nil { + L.Push(lua.LNil) + L.Push(lua.LString("Failed to begin transaction")) + return 2 + } + + ud := L.NewUserData() + ud.Value = tx + L.SetMetatable(ud, L.GetTypeMetatable("transaction")) + L.Push(ud) + return 1 + })) + + L.SetGlobal("commit_transaction", L.NewFunction(func(L *lua.LState) int { + if err := storage.CommitCurrentTransaction(); err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + L.Push(lua.LNil) + return 1 + })) + + L.SetGlobal("abort_transaction", L.NewFunction(func(L *lua.LState) int { + if err := storage.AbortCurrentTransaction(); err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + L.Push(lua.LNil) + return 1 + })) + + L.SetGlobal("has_active_transaction", L.NewFunction(func(L *lua.LState) int { + L.Push(lua.LBool(storage.HasActiveTransaction())) + return 1 + })) + + L.SetGlobal("get_current_transaction_id", L.NewFunction(func(L *lua.LState) int { + txID := storage.GetCurrentTransactionID() + if txID == "" { + L.Push(lua.LNil) + } else { + L.Push(lua.LString(txID)) + } + return 1 + })) + + L.SetGlobal("get_active_transactions", L.NewFunction(func(L *lua.LState) int { + txs := storage.GetActiveTransactions() + table := L.NewTable() + for i, tx := range txs { + txTable := L.NewTable() + txTable.RawSetString("id", lua.LString(tx.ID)) + txTable.RawSetString("status", lua.LString(tx.Status)) + txTable.RawSetString("start_time", lua.LNumber(tx.StartTime)) + txTable.RawSetString("operation_count", lua.LNumber(tx.OperationCount)) + table.RawSetInt(i+1, txTable) + } + L.Push(table) + return 1 + })) + + // Метатаблица для транзакций + mt := L.NewTypeMetatable("transaction") + L.SetField(mt, "__index", L.NewFunction(func(L *lua.LState) int { + ud := L.CheckUserData(1) + tx, ok := ud.Value.(*storage.Transaction) + if !ok { + L.Push(lua.LNil) + return 1 + } + method := L.CheckString(2) + + switch method { + case "get_id": + L.Push(lua.LString(fmt.Sprintf("%d", tx.ID))) + case "get_operation_count": + // Используем метод для получения количества операций + // Временное решение - получаем через reflection или добавляем метод в Transaction + L.Push(lua.LNumber(0)) + case "get_start_time": + L.Push(lua.LNumber(tx.StartTime)) + case "get_status": + status := "active" + // Получаем статус через State.Load() + L.Push(lua.LString(status)) + default: + L.Push(lua.LNil) + } + return 1 + })) +} + +// registerTriggerFunctions регистрирует функции для работы с триггерами +func (pm *PluginManager) registerTriggerFunctions(L *lua.LState) { + tm := storage.GetTriggerManager() + + L.SetGlobal("create_trigger", L.NewFunction(func(L *lua.LState) int { + database := L.CheckString(1) + collection := L.CheckString(2) + name := L.CheckString(3) + event := L.CheckString(4) + config := L.CheckTable(5) + + configMap := make(map[string]interface{}) + config.ForEach(func(key, value lua.LValue) { + if key.Type() == lua.LTString { + configMap[key.String()] = pm.luaValueToGo(value) + } + }) + + if err := tm.CreateTrigger(database, collection, name, storage.TriggerEvent(event), configMap); err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + L.Push(lua.LNil) + return 1 + })) + + L.SetGlobal("drop_trigger", L.NewFunction(func(L *lua.LState) int { + collection := L.CheckString(1) + event := L.CheckString(2) + name := L.CheckString(3) + + if err := tm.DropTrigger(collection, event, name); err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + L.Push(lua.LNil) + return 1 + })) + + L.SetGlobal("enable_trigger", L.NewFunction(func(L *lua.LState) int { + collection := L.CheckString(1) + event := L.CheckString(2) + name := L.CheckString(3) + + if err := tm.EnableTrigger(collection, event, name); err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + L.Push(lua.LNil) + return 1 + })) + + L.SetGlobal("disable_trigger", L.NewFunction(func(L *lua.LState) int { + collection := L.CheckString(1) + event := L.CheckString(2) + name := L.CheckString(3) + + if err := tm.DisableTrigger(collection, event, name); err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + L.Push(lua.LNil) + return 1 + })) + + L.SetGlobal("list_triggers", L.NewFunction(func(L *lua.LState) int { + collection := L.OptString(1, "") + triggers := tm.ListTriggers(collection) + + table := L.NewTable() + for i, trigger := range triggers { + triggerTable := L.NewTable() + triggerTable.RawSetString("name", lua.LString(trigger.Name)) + triggerTable.RawSetString("collection", lua.LString(trigger.Collection)) + triggerTable.RawSetString("event", lua.LString(string(trigger.Event))) + triggerTable.RawSetString("action", lua.LString(string(trigger.Action))) + triggerTable.RawSetString("enabled", lua.LBool(trigger.Enabled)) + triggerTable.RawSetString("description", lua.LString(trigger.Description)) + table.RawSetInt(i+1, triggerTable) + } + L.Push(table) + return 1 + })) +} + +// 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 "drop_collection": + L.Push(L.NewFunction(func(L *lua.LState) int { + name := L.CheckString(1) + err := db.DropCollection(name) + if err != nil { + L.Push(lua.LString(err.Error())) + return 1 + } + L.Push(lua.LNil) + return 1 + })) + case "list_collections": + L.Push(L.NewFunction(func(L *lua.LState) int { + collections := db.ListCollections() + table := L.NewTable() + for i, name := range collections { + table.RawSetInt(i+1, lua.LString(name)) + } + L.Push(table) + 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) + 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 + } + 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 + } + 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 "find_by_index": + L.Push(L.NewFunction(func(L *lua.LState) int { + indexName := L.CheckString(1) + value := pm.luaValueToGo(L.CheckAny(2)) + + docs, err := coll.FindByIndex(indexName, value) + if err != nil { + L.Push(lua.LNil) + L.Push(lua.LString(err.Error())) + return 2 + } + + table := L.NewTable() + for i, doc := range docs { + docTable := L.NewTable() + docTable.RawSetString("_id", lua.LString(doc.ID)) + for k, v := range doc.GetFields() { + docTable.RawSetString(k, pm.goValueToLua(L, v)) + } + table.RawSetInt(i+1, docTable) + } + 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 + })) + case "name": + L.Push(lua.LString(coll.Name())) + 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 + } + + 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)) + } + } + + 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 +} + +// GetPluginsDir возвращает директорию с плагинами +func (pm *PluginManager) GetPluginsDir() string { + return pm.pluginsDir +} + +// Version возвращает версию плагина +func (p *Plugin) Version() string { + return p.version +} + +// Author возвращает автора плагина +func (p *Plugin) Author() string { + return p.author +} + +// Description возвращает описание плагина +func (p *Plugin) Description() string { + return p.description +} + +// LoadedAt возвращает время загрузки плагина +func (p *Plugin) LoadedAt() time.Time { + return p.loadedAt +}