futriis/internal/lua/plugin.go

599 lines
17 KiB
Go
Raw Normal View History

2026-03-01 19:55:37 +00:00
// /futriis/internal/lua/plugin.go
// Пакет lua реализует систему плагинов на языке Lua для расширения функциональности СУБД.
// PluginManager управляет загрузкой, хранением и выполнением Lua-скриптов из указанной директории.
// Предоставляет мост между Go и Lua через регистрацию функций, доступных из скриптов.
// Позволяет динамически расширять возможности базы данных без перекомпиляции основного кода.
// Поддерживает возможность отключения через конфигурацию для минимизации ресурсов.
package lua
import (
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"sync"
"time"
"futriis/pkg/config"
"futriis/pkg/types"
"futriis/pkg/utils"
"github.com/yuin/gopher-lua"
)
// StorageInterface определяет интерфейс для взаимодействия с хранилищем из Lua плагинов
type StorageInterface interface {
// GetValue получает значение из хранилища по ключу
GetValue(key string) (interface{}, error)
// SetValue устанавливает значение в хранилище
SetValue(key string, value interface{}) error
// GetTuple получает кортеж по идентификатору
GetTuple(tappleName, sliceName, tupleID string) (*types.Tuple, error)
// CreateTuple создаёт новый кортеж
CreateTuple(tappleName, sliceName, tupleID string, fields map[string]interface{}) (*types.Tuple, error)
// UpdateTuple обновляет существующий кортеж
UpdateTuple(tappleName, sliceName, tupleID string, fields map[string]interface{}) (*types.Tuple, error)
// DeleteTuple удаляет кортеж
DeleteTuple(tappleName, sliceName, tupleID string) error
// FindTuples выполняет поиск кортежей по критериям
FindTuples(tappleName, sliceName string, criteria map[string]interface{}) ([]*types.Tuple, error)
}
// PluginManager управляет Lua плагинами
type PluginManager struct {
state *lua.LState
plugins map[string]*lua.LFunction
mu sync.RWMutex
enabled bool
storage StorageInterface // Интерфейс для доступа к хранилищу
recovered bool // Флаг, были ли восстановлены данные
}
// NewPluginManager создаёт новый менеджер плагинов
func NewPluginManager(cfg *config.LuaConfig) *PluginManager {
pm := &PluginManager{
plugins: make(map[string]*lua.LFunction),
enabled: cfg.Enabled,
recovered: false,
}
if cfg.Enabled {
pm.state = lua.NewState()
pm.registerFunctions()
// Загружаем плагины из директории
if err := pm.LoadPlugins(cfg.PluginsDir); err != nil {
// Добавляем пустую строку перед предупреждением
fmt.Println()
utils.PrintWarning("Failed to load Lua plugins: %v", err)
}
}
return pm
}
// SetStorage устанавливает интерфейс хранилища для плагинов
func (pm *PluginManager) SetStorage(storage StorageInterface) {
pm.mu.Lock()
defer pm.mu.Unlock()
pm.storage = storage
// Перерегистрируем функции с доступом к хранилищу
if pm.enabled && pm.state != nil && storage != nil {
pm.registerStorageFunctions()
}
}
// registerFunctions регистрирует базовые функции Go, доступные из Lua
func (pm *PluginManager) registerFunctions() {
if !pm.enabled || pm.state == nil {
return
}
// Регистрируем функцию print для вывода в консоль
pm.state.SetGlobal("print", pm.state.NewFunction(func(L *lua.LState) int {
top := L.GetTop()
args := make([]string, top)
for i := 1; i <= top; i++ {
args[i-1] = L.Get(i).String()
}
utils.PrintInfo(strings.Join(args, " "))
return 0
}))
// Регистрируем функцию type для получения типа значения
pm.state.SetGlobal("type", pm.state.NewFunction(func(L *lua.LState) int {
val := L.Get(1)
L.Push(lua.LString(val.Type().String()))
return 1
}))
// Регистрируем функцию tostring для преобразования в строку
pm.state.SetGlobal("tostring", pm.state.NewFunction(func(L *lua.LState) int {
val := L.Get(1)
L.Push(lua.LString(val.String()))
return 1
}))
// Регистрируем функцию error для генерации ошибки
pm.state.SetGlobal("error", pm.state.NewFunction(func(L *lua.LState) int {
msg := L.ToString(1)
L.RaiseError("%s", msg)
return 0
}))
}
// registerStorageFunctions регистрирует функции для работы с хранилищем
func (pm *PluginManager) registerStorageFunctions() {
if !pm.enabled || pm.state == nil || pm.storage == nil {
return
}
// Регистрируем функцию get для получения значения из хранилища
pm.state.SetGlobal("get", pm.state.NewFunction(func(L *lua.LState) int {
if pm.storage == nil {
L.RaiseError("storage not available")
return 0
}
key := L.ToString(1)
value, err := pm.storage.GetValue(key)
if err != nil {
L.Push(lua.LNil)
return 1
}
// Конвертируем значение в Lua тип
luaValue := pm.toLuaValue(L, value)
L.Push(luaValue)
return 1
}))
// Регистрируем функцию set для установки значения в хранилище
pm.state.SetGlobal("set", pm.state.NewFunction(func(L *lua.LState) int {
if pm.storage == nil {
L.RaiseError("storage not available")
return 0
}
key := L.ToString(1)
value := L.Get(2)
// Конвертируем Lua значение в Go значение
goValue := pm.fromLuaValue(value)
err := pm.storage.SetValue(key, goValue)
if err != nil {
L.RaiseError("failed to set value: %v", err)
return 0
}
utils.PrintInfo("Lua set: %s = %v", key, goValue)
return 0
}))
// Регистрируем функцию get_tuple для получения кортежа
pm.state.SetGlobal("get_tuple", pm.state.NewFunction(func(L *lua.LState) int {
if pm.storage == nil {
L.RaiseError("storage not available")
return 0
}
tappleName := L.ToString(1)
sliceName := L.ToString(2)
tupleID := L.ToString(3)
tuple, err := pm.storage.GetTuple(tappleName, sliceName, tupleID)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
// Конвертируем кортеж в Lua таблицу
tupleTable := pm.tupleToLuaTable(L, tuple)
L.Push(tupleTable)
L.Push(lua.LNil)
return 2
}))
// Регистрируем функцию create_tuple для создания кортежа
pm.state.SetGlobal("create_tuple", pm.state.NewFunction(func(L *lua.LState) int {
if pm.storage == nil {
L.RaiseError("storage not available")
return 0
}
tappleName := L.ToString(1)
sliceName := L.ToString(2)
tupleID := L.ToString(3)
// Получаем таблицу с полями
fieldsTable := L.ToTable(4)
fields := pm.luaTableToMap(fieldsTable)
tuple, err := pm.storage.CreateTuple(tappleName, sliceName, tupleID, fields)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
tupleTable := pm.tupleToLuaTable(L, tuple)
L.Push(tupleTable)
L.Push(lua.LNil)
return 2
}))
// Регистрируем функцию update_tuple для обновления кортежа
pm.state.SetGlobal("update_tuple", pm.state.NewFunction(func(L *lua.LState) int {
if pm.storage == nil {
L.RaiseError("storage not available")
return 0
}
tappleName := L.ToString(1)
sliceName := L.ToString(2)
tupleID := L.ToString(3)
fieldsTable := L.ToTable(4)
fields := pm.luaTableToMap(fieldsTable)
tuple, err := pm.storage.UpdateTuple(tappleName, sliceName, tupleID, fields)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
tupleTable := pm.tupleToLuaTable(L, tuple)
L.Push(tupleTable)
L.Push(lua.LNil)
return 2
}))
// Регистрируем функцию delete_tuple для удаления кортежа
pm.state.SetGlobal("delete_tuple", pm.state.NewFunction(func(L *lua.LState) int {
if pm.storage == nil {
L.RaiseError("storage not available")
return 0
}
tappleName := L.ToString(1)
sliceName := L.ToString(2)
tupleID := L.ToString(3)
err := pm.storage.DeleteTuple(tappleName, sliceName, tupleID)
if err != nil {
L.Push(lua.LFalse)
L.Push(lua.LString(err.Error()))
return 2
}
L.Push(lua.LTrue)
L.Push(lua.LNil)
return 2
}))
// Регистрируем функцию find_tuples для поиска кортежей
pm.state.SetGlobal("find_tuples", pm.state.NewFunction(func(L *lua.LState) int {
if pm.storage == nil {
L.RaiseError("storage not available")
return 0
}
tappleName := L.ToString(1)
sliceName := L.ToString(2)
criteriaTable := L.ToTable(3)
criteria := pm.luaTableToMap(criteriaTable)
tuples, err := pm.storage.FindTuples(tappleName, sliceName, criteria)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
// Создаём таблицу с результатами
resultTable := L.NewTable()
for i, tuple := range tuples {
tupleTable := pm.tupleToLuaTable(L, tuple)
L.RawSet(resultTable, lua.LNumber(i+1), tupleTable)
}
L.Push(resultTable)
L.Push(lua.LNil)
return 2
}))
}
// toLuaValue конвертирует Go значение в Lua значение
func (pm *PluginManager) toLuaValue(L *lua.LState, value interface{}) lua.LValue {
if value == nil {
return lua.LNil
}
switch v := value.(type) {
case string:
return lua.LString(v)
case int:
return lua.LNumber(v)
case int64:
return lua.LNumber(v)
case float64:
return lua.LNumber(v)
case bool:
if v {
return lua.LTrue
}
return lua.LFalse
case map[string]interface{}:
table := L.NewTable()
for key, val := range v {
L.RawSet(table, lua.LString(key), pm.toLuaValue(L, val))
}
return table
case []interface{}:
table := L.NewTable()
for i, val := range v {
L.RawSet(table, lua.LNumber(i+1), pm.toLuaValue(L, val))
}
return table
default:
return lua.LString(fmt.Sprintf("%v", v))
}
}
// fromLuaValue конвертирует Lua значение в Go значение
func (pm *PluginManager) fromLuaValue(value lua.LValue) interface{} {
switch v := value.(type) {
case lua.LString:
return string(v)
case lua.LNumber:
return float64(v)
case lua.LBool:
return bool(v)
case *lua.LTable:
// Определяем, является ли таблица массивом или словарём
// Пытаемся определить по наличию числовых ключей
isArray := false
var maxNum lua.LNumber = 0
v.ForEach(func(key lua.LValue, val lua.LValue) {
if numKey, ok := key.(lua.LNumber); ok {
isArray = true
if numKey > maxNum {
maxNum = numKey
}
}
})
if isArray && maxNum > 0 {
// Это массив
result := make([]interface{}, 0, int(maxNum))
// Заполняем массив, учитывая что индексы в Lua начинаются с 1
for i := lua.LNumber(1); i <= maxNum; i++ {
val := v.RawGet(i)
if val != lua.LNil {
result = append(result, pm.fromLuaValue(val))
}
}
return result
} else {
// Это словарь
result := make(map[string]interface{})
v.ForEach(func(key lua.LValue, val lua.LValue) {
if strKey, ok := key.(lua.LString); ok {
result[string(strKey)] = pm.fromLuaValue(val)
}
})
return result
}
default:
// Для всех остальных типов (включая nil) возвращаем строковое представление
return v.String()
}
}
// luaTableToMap конвертирует Lua таблицу в map[string]interface{}
func (pm *PluginManager) luaTableToMap(table *lua.LTable) map[string]interface{} {
result := make(map[string]interface{})
if table == nil {
return result
}
table.ForEach(func(key lua.LValue, value lua.LValue) {
if strKey, ok := key.(lua.LString); ok {
result[string(strKey)] = pm.fromLuaValue(value)
}
})
return result
}
// tupleToLuaTable конвертирует кортеж в Lua таблицу
func (pm *PluginManager) tupleToLuaTable(L *lua.LState, tuple *types.Tuple) *lua.LTable {
table := L.NewTable()
if tuple == nil {
return table
}
// Добавляем ID
L.RawSet(table, lua.LString("id"), lua.LString(tuple.ID))
// Добавляем поля
fieldsTable := L.NewTable()
for key, value := range tuple.Fields {
L.RawSet(fieldsTable, lua.LString(key), pm.toLuaValue(L, value))
}
L.RawSet(table, lua.LString("fields"), fieldsTable)
// Добавляем метаданные, если есть
var zeroTime time.Time
if tuple.CreatedAt != zeroTime {
L.RawSet(table, lua.LString("created_at"), lua.LString(tuple.CreatedAt.String()))
}
if tuple.UpdatedAt != zeroTime {
L.RawSet(table, lua.LString("updated_at"), lua.LString(tuple.UpdatedAt.String()))
}
return table
}
// LoadPlugins загружает все Lua плагины из директории
func (pm *PluginManager) LoadPlugins(pluginsDir string) error {
if !pm.enabled || pm.state == nil {
return nil
}
files, err := ioutil.ReadDir(pluginsDir)
if err != nil {
return fmt.Errorf("failed to read plugins directory: %v", err)
}
loadedCount := 0
for _, file := range files {
if !file.IsDir() && filepath.Ext(file.Name()) == ".lua" {
if err := pm.LoadPlugin(filepath.Join(pluginsDir, file.Name())); err != nil {
utils.PrintError("Error loading plugin %s: %v", file.Name(), err)
} else {
loadedCount++
}
}
}
if loadedCount > 0 {
utils.PrintSuccess("Loaded %d Lua plugin(s)", loadedCount)
}
return nil
}
// LoadPlugin загружает один Lua плагин
func (pm *PluginManager) LoadPlugin(path string) error {
if !pm.enabled || pm.state == nil {
return nil
}
data, err := ioutil.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read plugin file: %v", err)
}
// Загружаем строку как функцию
fn, err := pm.state.LoadString(string(data))
if err != nil {
return fmt.Errorf("failed to parse Lua plugin: %v", err)
}
pluginName := filepath.Base(path)
pm.mu.Lock()
pm.plugins[pluginName] = fn
pm.mu.Unlock()
utils.PrintSuccess("Loaded plugin: %s", pluginName)
return nil
}
// ExecutePlugin выполняет функцию плагина
func (pm *PluginManager) ExecutePlugin(name string, args ...lua.LValue) error {
if !pm.enabled || pm.state == nil {
return fmt.Errorf("Lua plugins are disabled")
}
pm.mu.RLock()
fn, exists := pm.plugins[name]
pm.mu.RUnlock()
if !exists {
return fmt.Errorf("plugin '%s' not found", name)
}
// Защита от паники в Lua коде
defer func() {
if r := recover(); r != nil {
utils.PrintError("Panic in Lua plugin: %v", r)
}
}()
pm.state.Push(fn)
for _, arg := range args {
pm.state.Push(arg)
}
// Выполняем с защитой от ошибок
if err := pm.state.PCall(len(args), lua.MultRet, nil); err != nil {
return fmt.Errorf("Lua execution error: %v", err)
}
return nil
}
// RecoverData восстанавливает данные из резервного хранилища
func (pm *PluginManager) RecoverData(backupStorage StorageInterface) error {
if !pm.enabled {
return fmt.Errorf("Lua plugins are disabled")
}
if pm.storage == nil {
return fmt.Errorf("main storage not available")
}
if backupStorage == nil {
return fmt.Errorf("backup storage not available")
}
utils.PrintInfo("Starting data recovery from backup...")
// Здесь должна быть логика восстановления данных
// В реальной реализации нужно пройти по всем кортежам в backupStorage
// и восстановить их в основном хранилище
// Для демонстрации просто устанавливаем флаг
pm.recovered = true
utils.PrintSuccess("Data recovery completed successfully")
return nil
}
// IsRecovered возвращает флаг восстановления данных
func (pm *PluginManager) IsRecovered() bool {
return pm.recovered
}
// GetStorage возвращает интерфейс хранилища
func (pm *PluginManager) GetStorage() StorageInterface {
pm.mu.RLock()
defer pm.mu.RUnlock()
return pm.storage
}
// Close закрывает Lua состояние и освобождает ресурсы
func (pm *PluginManager) Close() {
if pm.state != nil {
pm.mu.Lock()
defer pm.mu.Unlock()
// Очищаем плагины
pm.plugins = make(map[string]*lua.LFunction)
// Закрываем состояние Lua
pm.state.Close()
pm.state = nil
utils.PrintInfo("Lua plugin manager closed")
}
}