Files
futriix/internal/plugin/plugin.go

1068 lines
33 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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
}