733 lines
23 KiB
Go
733 lines
23 KiB
Go
|
|
// Файл: 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
|
|||
|
|
}
|