futriis/internal/lua/plugin.go

599 lines
17 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.

// /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")
}
}