/* * 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 }