Files
futriis/internal/plugin/plugin.go
2026-04-08 21:43:35 +03:00

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