first commit

This commit is contained in:
2026-04-08 21:43:35 +03:00
commit be7a1a3ea2
33 changed files with 9609 additions and 0 deletions

732
internal/plugin/plugin.go Normal file
View File

@@ -0,0 +1,732 @@
// Файл: 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
}