2401 lines
74 KiB
Go
2401 lines
74 KiB
Go
/*
|
||
* Copyright 2026 Safronov Grigorii
|
||
*
|
||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
*
|
||
* You may obtain a copy of the License at
|
||
* https://opensource.org/licenses/CDDL-1.0
|
||
*/
|
||
|
||
// Файл: internal/plugin/plugin.go
|
||
// Назначение: Система плагинов на основе Lua для расширения функциональности СУБД.
|
||
// Позволяет загружать Lua-скрипты как плагины, выполнять их в изолированном окружении,
|
||
// взаимодействовать с данными СУБД и логировать действия плагинов в общий лог-файл.
|
||
// УЛУЧШЕНИЯ: Sandbox с CPU/memory limits, Plugin Dependencies, Hot Reload (без fsnotify),
|
||
// Plugin Marketplace API, Пул Lua-состояний с ограничениями.
|
||
|
||
package plugin
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"runtime"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"futriis/internal/storage"
|
||
|
||
lua "github.com/yuin/gopher-lua"
|
||
)
|
||
|
||
// ========== Константы ==========
|
||
|
||
const (
|
||
DefaultCPULimit = 100 * time.Millisecond
|
||
DefaultMemoryLimit = 50 * 1024 * 1024
|
||
DefaultExecutionTimeout = 5 * time.Second
|
||
HotReloadCheckInterval = 30 * time.Second
|
||
MaxPluginLoadTime = 10 * time.Second
|
||
DefaultMaxEventLogSize = 1000
|
||
DefaultBatchSize = 100
|
||
DefaultWriteBufferSize = 64 * 1024
|
||
|
||
// Константы для пула Lua-состояний
|
||
MaxLuaStates = 100
|
||
LuaStateTTL = 10 * time.Minute
|
||
LuaStatePoolSize = 50
|
||
)
|
||
|
||
// PluginConfig представляет конфигурацию плагинов (добавлено для совместимости с main.go)
|
||
type PluginConfig struct {
|
||
Enabled bool `toml:"enabled"`
|
||
ScriptDir string `toml:"script_dir"`
|
||
AllowList []string `toml:"allow_list"`
|
||
MaxCPUMS int64 `toml:"max_cpu_ms"`
|
||
MaxMemoryMB int64 `toml:"max_memory_mb"`
|
||
}
|
||
|
||
// ========== Пул Lua-состояний ==========
|
||
|
||
// LuaStatePool управляет пулом Lua-состояний для ограничения ресурсов
|
||
type LuaStatePool struct {
|
||
states chan *InstrumentedLState
|
||
active atomic.Int32
|
||
maxSize int
|
||
mu sync.RWMutex
|
||
createdAt map[*InstrumentedLState]time.Time
|
||
closed bool
|
||
}
|
||
|
||
var globalStatePool *LuaStatePool
|
||
var statePoolOnce sync.Once
|
||
|
||
// GetLuaStatePool возвращает глобальный пул Lua-состояний
|
||
func GetLuaStatePool() *LuaStatePool {
|
||
statePoolOnce.Do(func() {
|
||
globalStatePool = &LuaStatePool{
|
||
states: make(chan *InstrumentedLState, LuaStatePoolSize),
|
||
maxSize: MaxLuaStates,
|
||
createdAt: make(map[*InstrumentedLState]time.Time),
|
||
}
|
||
})
|
||
return globalStatePool
|
||
}
|
||
|
||
// Acquire получает состояние из пула или создаёт новое
|
||
func (p *LuaStatePool) Acquire(limits SandboxLimits) (*InstrumentedLState, error) {
|
||
if p.closed {
|
||
return nil, fmt.Errorf("state pool is closed")
|
||
}
|
||
|
||
select {
|
||
case ls := <-p.states:
|
||
if ls == nil {
|
||
return p.Acquire(limits)
|
||
}
|
||
p.mu.RLock()
|
||
createdAt, ok := p.createdAt[ls]
|
||
p.mu.RUnlock()
|
||
|
||
if !ok || time.Since(createdAt) > LuaStateTTL {
|
||
ls.Close()
|
||
p.mu.Lock()
|
||
delete(p.createdAt, ls)
|
||
p.mu.Unlock()
|
||
p.active.Add(-1)
|
||
return p.Acquire(limits)
|
||
}
|
||
return ls, nil
|
||
default:
|
||
if p.active.Load() >= int32(p.maxSize) {
|
||
return nil, fmt.Errorf("max Lua states limit reached: %d", p.maxSize)
|
||
}
|
||
|
||
ls := NewInstrumentedLState(limits)
|
||
p.active.Add(1)
|
||
p.mu.Lock()
|
||
p.createdAt[ls] = time.Now()
|
||
p.mu.Unlock()
|
||
return ls, nil
|
||
}
|
||
}
|
||
|
||
// Release возвращает состояние в пул
|
||
func (p *LuaStatePool) Release(ls *InstrumentedLState) {
|
||
if ls == nil || p.closed {
|
||
if ls != nil {
|
||
ls.Close()
|
||
p.active.Add(-1)
|
||
}
|
||
return
|
||
}
|
||
|
||
ls.instructions.Store(0)
|
||
ls.startTime = time.Now()
|
||
|
||
select {
|
||
case p.states <- ls:
|
||
default:
|
||
ls.Close()
|
||
p.active.Add(-1)
|
||
p.mu.Lock()
|
||
delete(p.createdAt, ls)
|
||
p.mu.Unlock()
|
||
}
|
||
}
|
||
|
||
// GetActiveCount возвращает количество активных состояний
|
||
func (p *LuaStatePool) GetActiveCount() int32 {
|
||
return p.active.Load()
|
||
}
|
||
|
||
// GetPoolSize возвращает размер пула
|
||
func (p *LuaStatePool) GetPoolSize() int {
|
||
return len(p.states)
|
||
}
|
||
|
||
// Close закрывает пул и все состояния
|
||
func (p *LuaStatePool) Close() {
|
||
p.mu.Lock()
|
||
if p.closed {
|
||
p.mu.Unlock()
|
||
return
|
||
}
|
||
p.closed = true
|
||
p.mu.Unlock()
|
||
|
||
close(p.states)
|
||
for ls := range p.states {
|
||
if ls != nil {
|
||
ls.Close()
|
||
}
|
||
}
|
||
|
||
p.mu.Lock()
|
||
for ls := range p.createdAt {
|
||
if ls != nil {
|
||
ls.Close()
|
||
}
|
||
}
|
||
p.createdAt = make(map[*InstrumentedLState]time.Time)
|
||
p.mu.Unlock()
|
||
|
||
p.active.Store(0)
|
||
}
|
||
|
||
// ========== Sandbox с ограничениями ==========
|
||
|
||
// SandboxLimits определяет ограничения для Lua-скрипта
|
||
type SandboxLimits struct {
|
||
MaxCPUTime time.Duration
|
||
MaxMemory int64
|
||
MaxExecutionTime time.Duration
|
||
MaxStackDepth int
|
||
MaxInstructions int64
|
||
}
|
||
|
||
// InstrumentedLState расширяет lua.LState с мониторингом ресурсов
|
||
type InstrumentedLState struct {
|
||
*lua.LState
|
||
limits SandboxLimits
|
||
instructions atomic.Int64
|
||
startTime time.Time
|
||
ctx context.Context
|
||
cancel context.CancelFunc
|
||
mu sync.RWMutex
|
||
closed bool
|
||
}
|
||
|
||
// NewInstrumentedLState создаёт Lua-состояние с ограничениями
|
||
func NewInstrumentedLState(limits SandboxLimits) *InstrumentedLState {
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
|
||
ls := &InstrumentedLState{
|
||
LState: lua.NewState(lua.Options{
|
||
SkipOpenLibs: true,
|
||
}),
|
||
limits: limits,
|
||
startTime: time.Now(),
|
||
ctx: ctx,
|
||
cancel: cancel,
|
||
}
|
||
|
||
lua.OpenBase(ls.LState)
|
||
lua.OpenString(ls.LState)
|
||
lua.OpenTable(ls.LState)
|
||
lua.OpenMath(ls.LState)
|
||
|
||
runtime.SetFinalizer(ls, func(ls *InstrumentedLState) {
|
||
if ls != nil && !ls.closed {
|
||
ls.Close()
|
||
}
|
||
})
|
||
|
||
return ls
|
||
}
|
||
|
||
// SetCPULimit устанавливает лимит CPU времени
|
||
func (ls *InstrumentedLState) SetCPULimit(limit time.Duration) {
|
||
ls.mu.Lock()
|
||
defer ls.mu.Unlock()
|
||
ls.limits.MaxCPUTime = limit
|
||
}
|
||
|
||
// SetMemoryLimit устанавливает лимит памяти
|
||
func (ls *InstrumentedLState) SetMemoryLimit(limit int64) {
|
||
ls.mu.Lock()
|
||
defer ls.mu.Unlock()
|
||
ls.limits.MaxMemory = limit
|
||
}
|
||
|
||
// CheckMemory проверяет текущее использование памяти
|
||
func (ls *InstrumentedLState) CheckMemory() bool {
|
||
if ls.limits.MaxMemory <= 0 {
|
||
return true
|
||
}
|
||
var m runtime.MemStats
|
||
runtime.ReadMemStats(&m)
|
||
return int64(m.Alloc) <= ls.limits.MaxMemory
|
||
}
|
||
|
||
// GetSandboxStats возвращает статистику песочницы
|
||
func (ls *InstrumentedLState) GetSandboxStats() map[string]interface{} {
|
||
var m runtime.MemStats
|
||
runtime.ReadMemStats(&m)
|
||
|
||
return map[string]interface{}{
|
||
"instructions": ls.instructions.Load(),
|
||
"execution_ms": time.Since(ls.startTime).Milliseconds(),
|
||
"memory_alloc": m.Alloc,
|
||
"memory_sys": m.Sys,
|
||
"num_gc": m.NumGC,
|
||
"pause_total_ms": m.PauseTotalNs / 1000000,
|
||
}
|
||
}
|
||
|
||
// IsClosed возвращает статус закрытия
|
||
func (ls *InstrumentedLState) IsClosed() bool {
|
||
ls.mu.RLock()
|
||
defer ls.mu.RUnlock()
|
||
return ls.closed
|
||
}
|
||
|
||
// Close закрывает Lua состояние
|
||
func (ls *InstrumentedLState) Close() {
|
||
ls.mu.Lock()
|
||
defer ls.mu.Unlock()
|
||
|
||
if ls.closed {
|
||
return
|
||
}
|
||
ls.closed = true
|
||
ls.cancel()
|
||
if ls.LState != nil {
|
||
ls.LState.Close()
|
||
}
|
||
}
|
||
|
||
// ========== Plugin Dependencies ==========
|
||
|
||
// PluginDependency определяет зависимость плагина
|
||
type PluginDependency struct {
|
||
Name string `json:"name"`
|
||
Version string `json:"version"`
|
||
MinVersion string `json:"min_version,omitempty"`
|
||
MaxVersion string `json:"max_version,omitempty"`
|
||
Optional bool `json:"optional,omitempty"`
|
||
}
|
||
|
||
// PluginManifest описывает метаданные плагина
|
||
type PluginManifest struct {
|
||
Name string `json:"name"`
|
||
Version string `json:"version"`
|
||
Author string `json:"author"`
|
||
Description string `json:"description"`
|
||
Dependencies []PluginDependency `json:"dependencies,omitempty"`
|
||
APIVersion string `json:"api_version"`
|
||
MinGoVersion string `json:"min_go_version,omitempty"`
|
||
EntryPoint string `json:"entry_point,omitempty"`
|
||
CreatedAt int64 `json:"created_at"`
|
||
UpdatedAt int64 `json:"updated_at"`
|
||
}
|
||
|
||
// PluginWithDeps расширяет Plugin с поддержкой зависимостей
|
||
type PluginWithDeps struct {
|
||
*Plugin
|
||
Manifest *PluginManifest
|
||
Dependencies map[string]*PluginWithDeps
|
||
Dependents map[string]*PluginWithDeps
|
||
LoadOrder int
|
||
depMu sync.RWMutex
|
||
}
|
||
|
||
// DependencyResolver разрешает зависимости между плагинами
|
||
type DependencyResolver struct {
|
||
plugins map[string]*PluginWithDeps
|
||
mu sync.RWMutex
|
||
}
|
||
|
||
// NewDependencyResolver создаёт новый резолвер зависимостей
|
||
func NewDependencyResolver() *DependencyResolver {
|
||
return &DependencyResolver{
|
||
plugins: make(map[string]*PluginWithDeps),
|
||
}
|
||
}
|
||
|
||
// RegisterPlugin регистрирует плагин с его манифестом
|
||
func (dr *DependencyResolver) RegisterPlugin(plugin *PluginWithDeps) error {
|
||
dr.mu.Lock()
|
||
defer dr.mu.Unlock()
|
||
|
||
dr.plugins[plugin.Name] = plugin
|
||
return nil
|
||
}
|
||
|
||
// ResolveOrder возвращает порядок загрузки плагинов (топологическая сортировка)
|
||
func (dr *DependencyResolver) ResolveOrder() ([]*PluginWithDeps, error) {
|
||
dr.mu.RLock()
|
||
defer dr.mu.RUnlock()
|
||
|
||
graph := make(map[string][]string)
|
||
inDegree := make(map[string]int)
|
||
|
||
for name, plugin := range dr.plugins {
|
||
inDegree[name] = 0
|
||
for _, dep := range plugin.Manifest.Dependencies {
|
||
if !dep.Optional {
|
||
graph[dep.Name] = append(graph[dep.Name], name)
|
||
inDegree[name]++
|
||
}
|
||
}
|
||
}
|
||
|
||
queue := make([]string, 0)
|
||
for name, degree := range inDegree {
|
||
if degree == 0 {
|
||
queue = append(queue, name)
|
||
}
|
||
}
|
||
|
||
result := make([]*PluginWithDeps, 0)
|
||
for len(queue) > 0 {
|
||
current := queue[0]
|
||
queue = queue[1:]
|
||
|
||
result = append(result, dr.plugins[current])
|
||
|
||
for _, dependent := range graph[current] {
|
||
inDegree[dependent]--
|
||
if inDegree[dependent] == 0 {
|
||
queue = append(queue, dependent)
|
||
}
|
||
}
|
||
}
|
||
|
||
if len(result) != len(dr.plugins) {
|
||
return nil, fmt.Errorf("circular dependency detected among plugins")
|
||
}
|
||
|
||
for i, plugin := range result {
|
||
plugin.LoadOrder = i
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// CheckVersionCompatibility проверяет совместимость версий
|
||
func (dr *DependencyResolver) CheckVersionCompatibility(dep PluginDependency, actualVersion string) bool {
|
||
if dep.MinVersion != "" && actualVersion < dep.MinVersion {
|
||
return false
|
||
}
|
||
if dep.MaxVersion != "" && actualVersion > dep.MaxVersion {
|
||
return false
|
||
}
|
||
if dep.Version != "" && actualVersion != dep.Version {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
// ========== File Watcher (без fsnotify) ==========
|
||
|
||
// FileWatcher самостоятельно отслеживает изменения файлов
|
||
type FileWatcher struct {
|
||
files map[string]time.Time
|
||
mu sync.RWMutex
|
||
interval time.Duration
|
||
onChange func(path string) error
|
||
stopChan chan struct{}
|
||
wg sync.WaitGroup
|
||
logger storage.LoggerInterface
|
||
}
|
||
|
||
// NewFileWatcher создаёт новый файловый вотчер без fsnotify
|
||
func NewFileWatcher(interval time.Duration, onChange func(path string) error, logger storage.LoggerInterface) *FileWatcher {
|
||
if interval <= 0 {
|
||
interval = HotReloadCheckInterval
|
||
}
|
||
return &FileWatcher{
|
||
files: make(map[string]time.Time),
|
||
interval: interval,
|
||
onChange: onChange,
|
||
stopChan: make(chan struct{}),
|
||
logger: logger,
|
||
}
|
||
}
|
||
|
||
// Add отслеживает файл
|
||
func (fw *FileWatcher) Add(path string) error {
|
||
info, err := os.Stat(path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
fw.mu.Lock()
|
||
defer fw.mu.Unlock()
|
||
fw.files[path] = info.ModTime()
|
||
return nil
|
||
}
|
||
|
||
// Remove прекращает отслеживание файла
|
||
func (fw *FileWatcher) Remove(path string) {
|
||
fw.mu.Lock()
|
||
defer fw.mu.Unlock()
|
||
delete(fw.files, path)
|
||
}
|
||
|
||
// Start запускает цикл проверки изменений
|
||
func (fw *FileWatcher) Start() {
|
||
fw.wg.Add(1)
|
||
go fw.watchLoop()
|
||
}
|
||
|
||
// Stop останавливает цикл проверки
|
||
func (fw *FileWatcher) Stop() {
|
||
close(fw.stopChan)
|
||
fw.wg.Wait()
|
||
}
|
||
|
||
// watchLoop периодически проверяет изменения файлов
|
||
func (fw *FileWatcher) watchLoop() {
|
||
defer fw.wg.Done()
|
||
|
||
ticker := time.NewTicker(fw.interval)
|
||
defer ticker.Stop()
|
||
|
||
for {
|
||
select {
|
||
case <-ticker.C:
|
||
fw.checkChanges()
|
||
case <-fw.stopChan:
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// checkChanges проверяет изменения всех отслеживаемых файлов
|
||
func (fw *FileWatcher) checkChanges() {
|
||
fw.mu.RLock()
|
||
files := make([]string, 0, len(fw.files))
|
||
for path := range fw.files {
|
||
files = append(files, path)
|
||
}
|
||
fw.mu.RUnlock()
|
||
|
||
for _, path := range files {
|
||
info, err := os.Stat(path)
|
||
if err != nil {
|
||
if fw.logger != nil {
|
||
fw.logger.Warn(fmt.Sprintf("Failed to stat file %s: %v", path, err))
|
||
}
|
||
continue
|
||
}
|
||
|
||
fw.mu.RLock()
|
||
lastMod := fw.files[path]
|
||
fw.mu.RUnlock()
|
||
|
||
if info.ModTime().After(lastMod) {
|
||
if fw.logger != nil {
|
||
fw.logger.Info(fmt.Sprintf("File changed: %s (was %v, now %v)", path, lastMod, info.ModTime()))
|
||
}
|
||
|
||
if err := fw.onChange(path); err != nil {
|
||
if fw.logger != nil {
|
||
fw.logger.Error(fmt.Sprintf("Failed to handle change for %s: %v", path, err))
|
||
}
|
||
}
|
||
|
||
fw.mu.Lock()
|
||
fw.files[path] = info.ModTime()
|
||
fw.mu.Unlock()
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========== HotReloadManager управляет горячей перезагрузкой плагинов ==========
|
||
|
||
type HotReloadManager struct {
|
||
pluginManager *PluginManager
|
||
watcher *FileWatcher
|
||
reloadQueue chan string
|
||
reloading map[string]bool
|
||
mu sync.RWMutex
|
||
logger storage.LoggerInterface
|
||
}
|
||
|
||
// NewHotReloadManager создаёт менеджер горячей перезагрузки
|
||
func NewHotReloadManager(pm *PluginManager, logger storage.LoggerInterface) *HotReloadManager {
|
||
hrm := &HotReloadManager{
|
||
pluginManager: pm,
|
||
reloadQueue: make(chan string, 100),
|
||
reloading: make(map[string]bool),
|
||
logger: logger,
|
||
}
|
||
|
||
hrm.watcher = NewFileWatcher(HotReloadCheckInterval, hrm.handleFileChange, logger)
|
||
|
||
go hrm.processReloadQueue()
|
||
|
||
return hrm
|
||
}
|
||
|
||
// Start запускает мониторинг изменений
|
||
func (hrm *HotReloadManager) Start() {
|
||
hrm.watcher.Start()
|
||
}
|
||
|
||
// Stop останавливает мониторинг
|
||
func (hrm *HotReloadManager) Stop() {
|
||
hrm.watcher.Stop()
|
||
close(hrm.reloadQueue)
|
||
}
|
||
|
||
// WatchPlugin начинает отслеживать плагин
|
||
func (hrm *HotReloadManager) WatchPlugin(pluginPath string) error {
|
||
return hrm.watcher.Add(pluginPath)
|
||
}
|
||
|
||
// handleFileChange обрабатывает изменение файла
|
||
func (hrm *HotReloadManager) handleFileChange(path string) error {
|
||
baseName := filepath.Base(path)
|
||
pluginName := strings.TrimSuffix(baseName, ".lua")
|
||
|
||
hrm.mu.Lock()
|
||
if hrm.reloading[pluginName] {
|
||
hrm.mu.Unlock()
|
||
return nil
|
||
}
|
||
hrm.reloading[pluginName] = true
|
||
hrm.mu.Unlock()
|
||
|
||
select {
|
||
case hrm.reloadQueue <- pluginName:
|
||
default:
|
||
if hrm.logger != nil {
|
||
hrm.logger.Warn(fmt.Sprintf("Reload queue full, skipping reload for %s", pluginName))
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// processReloadQueue обрабатывает очередь перезагрузки
|
||
func (hrm *HotReloadManager) processReloadQueue() {
|
||
for pluginName := range hrm.reloadQueue {
|
||
hrm.reloadPlugin(pluginName)
|
||
|
||
hrm.mu.Lock()
|
||
delete(hrm.reloading, pluginName)
|
||
hrm.mu.Unlock()
|
||
}
|
||
}
|
||
|
||
// reloadPlugin выполняет горячую перезагрузку плагина
|
||
func (hrm *HotReloadManager) reloadPlugin(pluginName string) {
|
||
if hrm.logger != nil {
|
||
hrm.logger.Info(fmt.Sprintf("Hot reloading plugin: %s", pluginName))
|
||
}
|
||
|
||
oldPlugin, err := hrm.pluginManager.GetPlugin(pluginName)
|
||
if err != nil {
|
||
if hrm.logger != nil {
|
||
hrm.logger.Error(fmt.Sprintf("Plugin not found for reload: %s", pluginName))
|
||
}
|
||
return
|
||
}
|
||
|
||
var oldState map[string]interface{}
|
||
|
||
if oldWithDeps, ok := interface{}(oldPlugin).(*PluginWithDeps); ok {
|
||
if oldWithDeps.LState != nil && !oldWithDeps.IsClosed() {
|
||
fn := oldWithDeps.LState.GetGlobal("on_before_reload")
|
||
if fn != lua.LNil {
|
||
oldWithDeps.LState.CallByParam(lua.P{
|
||
Fn: fn,
|
||
NRet: 1,
|
||
Protect: true,
|
||
})
|
||
ret := oldWithDeps.LState.Get(-1)
|
||
if table, ok := ret.(*lua.LTable); ok {
|
||
oldState = make(map[string]interface{})
|
||
table.ForEach(func(key, value lua.LValue) {
|
||
oldState[key.String()] = value.String()
|
||
})
|
||
}
|
||
oldWithDeps.LState.Pop(1)
|
||
}
|
||
}
|
||
} else if oldPlugin.LState != nil && !oldPlugin.IsClosed() {
|
||
fn := oldPlugin.LState.GetGlobal("on_before_reload")
|
||
if fn != lua.LNil {
|
||
oldPlugin.LState.CallByParam(lua.P{
|
||
Fn: fn,
|
||
NRet: 1,
|
||
Protect: true,
|
||
})
|
||
ret := oldPlugin.LState.Get(-1)
|
||
if table, ok := ret.(*lua.LTable); ok {
|
||
oldState = make(map[string]interface{})
|
||
table.ForEach(func(key, value lua.LValue) {
|
||
oldState[key.String()] = value.String()
|
||
})
|
||
}
|
||
oldPlugin.LState.Pop(1)
|
||
}
|
||
}
|
||
|
||
if err := hrm.pluginManager.StopPlugin(pluginName); err != nil {
|
||
if hrm.logger != nil {
|
||
hrm.logger.Warn(fmt.Sprintf("Failed to stop plugin before reload: %v", err))
|
||
}
|
||
}
|
||
|
||
if err := hrm.pluginManager.UnloadPlugin(pluginName); err != nil {
|
||
if hrm.logger != nil {
|
||
hrm.logger.Error(fmt.Sprintf("Failed to unload plugin for reload: %v", err))
|
||
}
|
||
return
|
||
}
|
||
|
||
pluginPath := filepath.Join(hrm.pluginManager.GetPluginsDir(), pluginName+".lua")
|
||
if err := hrm.pluginManager.LoadPluginWithSandbox(pluginName, pluginPath, SandboxLimits{
|
||
MaxCPUTime: DefaultCPULimit,
|
||
MaxMemory: DefaultMemoryLimit,
|
||
MaxExecutionTime: DefaultExecutionTimeout,
|
||
MaxInstructions: 1000000,
|
||
}); err != nil {
|
||
if hrm.logger != nil {
|
||
hrm.logger.Error(fmt.Sprintf("Failed to load plugin during reload: %v", err))
|
||
}
|
||
return
|
||
}
|
||
|
||
newPlugin, _ := hrm.pluginManager.GetPlugin(pluginName)
|
||
|
||
if newWithDeps, ok := interface{}(newPlugin).(*PluginWithDeps); ok && oldState != nil {
|
||
if newWithDeps.LState != nil && !newWithDeps.IsClosed() {
|
||
fn := newWithDeps.LState.GetGlobal("on_after_reload")
|
||
if fn != lua.LNil {
|
||
stateTable := newWithDeps.LState.NewTable()
|
||
for k, v := range oldState {
|
||
stateTable.RawSetString(k, lua.LString(fmt.Sprintf("%v", v)))
|
||
}
|
||
newWithDeps.LState.CallByParam(lua.P{
|
||
Fn: fn,
|
||
NRet: 0,
|
||
Protect: true,
|
||
}, stateTable)
|
||
}
|
||
}
|
||
} else if newPlugin != nil && newPlugin.LState != nil && !newPlugin.IsClosed() && oldState != nil {
|
||
fn := newPlugin.LState.GetGlobal("on_after_reload")
|
||
if fn != lua.LNil {
|
||
stateTable := newPlugin.LState.NewTable()
|
||
for k, v := range oldState {
|
||
stateTable.RawSetString(k, lua.LString(fmt.Sprintf("%v", v)))
|
||
}
|
||
newPlugin.LState.CallByParam(lua.P{
|
||
Fn: fn,
|
||
NRet: 0,
|
||
Protect: true,
|
||
}, stateTable)
|
||
}
|
||
}
|
||
|
||
if err := hrm.pluginManager.StartPlugin(pluginName); err != nil {
|
||
if hrm.logger != nil {
|
||
hrm.logger.Error(fmt.Sprintf("Failed to start plugin after reload: %v", err))
|
||
}
|
||
}
|
||
|
||
if hrm.logger != nil {
|
||
hrm.logger.Info(fmt.Sprintf("Plugin hot reload completed: %s", pluginName))
|
||
}
|
||
|
||
storage.LogAudit("PLUGIN_HOT_RELOAD", "PLUGIN", pluginName, map[string]interface{}{
|
||
"reload_time": time.Now().UnixMilli(),
|
||
})
|
||
}
|
||
|
||
// ========== Plugin Marketplace API ==========
|
||
|
||
// MarketplacePlugin представляет плагин в маркетплейсе
|
||
type MarketplacePlugin struct {
|
||
ID string `json:"id"`
|
||
Name string `json:"name"`
|
||
Version string `json:"version"`
|
||
Author string `json:"author"`
|
||
Description string `json:"description"`
|
||
Downloads int64 `json:"downloads"`
|
||
Rating float64 `json:"rating"`
|
||
Tags []string `json:"tags"`
|
||
Repository string `json:"repository"`
|
||
CreatedAt int64 `json:"created_at"`
|
||
UpdatedAt int64 `json:"updated_at"`
|
||
}
|
||
|
||
// PluginMarketplace управляет плагинами из маркетплейса
|
||
type PluginMarketplace struct {
|
||
plugins map[string]*MarketplacePlugin
|
||
installed map[string]bool
|
||
apiEndpoint string
|
||
mu sync.RWMutex
|
||
logger storage.LoggerInterface
|
||
pm *PluginManager
|
||
}
|
||
|
||
// NewPluginMarketplace создаёт новый маркетплейс
|
||
func NewPluginMarketplace(apiEndpoint string, pm *PluginManager, logger storage.LoggerInterface) *PluginMarketplace {
|
||
return &PluginMarketplace{
|
||
plugins: make(map[string]*MarketplacePlugin),
|
||
installed: make(map[string]bool),
|
||
apiEndpoint: apiEndpoint,
|
||
logger: logger,
|
||
pm: pm,
|
||
}
|
||
}
|
||
|
||
// ListAvailablePlugins возвращает список доступных плагинов
|
||
func (pmkt *PluginMarketplace) ListAvailablePlugins() []*MarketplacePlugin {
|
||
pmkt.mu.RLock()
|
||
defer pmkt.mu.RUnlock()
|
||
|
||
result := make([]*MarketplacePlugin, 0, len(pmkt.plugins))
|
||
for _, p := range pmkt.plugins {
|
||
result = append(result, p)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// RegisterPlugin регистрирует плагин в маркетплейсе
|
||
func (pmkt *PluginMarketplace) RegisterPlugin(plugin *MarketplacePlugin) {
|
||
pmkt.mu.Lock()
|
||
defer pmkt.mu.Unlock()
|
||
pmkt.plugins[plugin.ID] = plugin
|
||
}
|
||
|
||
// InstallPlugin устанавливает плагин из маркетплейса
|
||
func (pmkt *PluginMarketplace) InstallPlugin(pluginID, version string) error {
|
||
pmkt.mu.RLock()
|
||
plugin, ok := pmkt.plugins[pluginID]
|
||
pmkt.mu.RUnlock()
|
||
|
||
if !ok {
|
||
return fmt.Errorf("plugin not found in marketplace: %s", pluginID)
|
||
}
|
||
|
||
pluginPath := filepath.Join(pmkt.pm.GetPluginsDir(), plugin.Name+".lua")
|
||
|
||
if err := pmkt.pm.LoadPluginWithSandbox(plugin.Name, pluginPath, SandboxLimits{
|
||
MaxCPUTime: DefaultCPULimit,
|
||
MaxMemory: DefaultMemoryLimit,
|
||
MaxExecutionTime: DefaultExecutionTimeout,
|
||
MaxInstructions: 1000000,
|
||
}); err != nil {
|
||
return fmt.Errorf("failed to load plugin: %v", err)
|
||
}
|
||
|
||
if err := pmkt.pm.StartPlugin(plugin.Name); err != nil {
|
||
return fmt.Errorf("failed to start plugin: %v", err)
|
||
}
|
||
|
||
pmkt.mu.Lock()
|
||
pmkt.installed[pluginID] = true
|
||
pmkt.mu.Unlock()
|
||
|
||
if pmkt.logger != nil {
|
||
pmkt.logger.Info(fmt.Sprintf("Installed plugin from marketplace: %s v%s", plugin.Name, plugin.Version))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// UninstallPlugin удаляет плагин
|
||
func (pmkt *PluginMarketplace) UninstallPlugin(pluginID string) error {
|
||
pmkt.mu.RLock()
|
||
plugin, ok := pmkt.plugins[pluginID]
|
||
pmkt.mu.RUnlock()
|
||
|
||
if !ok {
|
||
return fmt.Errorf("plugin not found: %s", pluginID)
|
||
}
|
||
|
||
if err := pmkt.pm.StopPlugin(plugin.Name); err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := pmkt.pm.UnloadPlugin(plugin.Name); err != nil {
|
||
return err
|
||
}
|
||
|
||
pmkt.mu.Lock()
|
||
delete(pmkt.installed, pluginID)
|
||
pmkt.mu.Unlock()
|
||
|
||
return nil
|
||
}
|
||
|
||
// UpdatePlugin обновляет плагин до новой версии
|
||
func (pmkt *PluginMarketplace) UpdatePlugin(pluginID, newVersion string) error {
|
||
if err := pmkt.UninstallPlugin(pluginID); err != nil {
|
||
return err
|
||
}
|
||
return pmkt.InstallPlugin(pluginID, newVersion)
|
||
}
|
||
|
||
// GetInstalledPlugins возвращает список установленных плагинов
|
||
func (pmkt *PluginMarketplace) GetInstalledPlugins() []string {
|
||
pmkt.mu.RLock()
|
||
defer pmkt.mu.RUnlock()
|
||
|
||
result := make([]string, 0, len(pmkt.installed))
|
||
for id := range pmkt.installed {
|
||
result = append(result, id)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// ========== Plugin Struct ==========
|
||
|
||
// PluginStatus представляет состояние плагина
|
||
type PluginStatus int32
|
||
|
||
const (
|
||
StatusLoaded PluginStatus = iota
|
||
StatusRunning
|
||
StatusStopped
|
||
StatusError
|
||
)
|
||
|
||
// PluginEventLog представляет событие плагина с временными метками
|
||
type PluginEventLog struct {
|
||
PluginName string `json:"plugin_name"`
|
||
EventType string `json:"event_type"`
|
||
Data interface{} `json:"data"`
|
||
Timestamp int64 `json:"timestamp"`
|
||
TimestampStr string `json:"timestamp_str"`
|
||
DurationMs int64 `json:"duration_ms,omitempty"`
|
||
Error string `json:"error,omitempty"`
|
||
}
|
||
|
||
// Plugin представляет загруженный Lua-плагин
|
||
type Plugin struct {
|
||
Name string
|
||
FilePath string
|
||
Status atomic.Int32
|
||
LState *lua.LState
|
||
logger storage.LoggerInterface
|
||
storage *storage.Storage
|
||
mu sync.RWMutex
|
||
loadedAt time.Time
|
||
loadedAtMs int64
|
||
loadedAtStr string
|
||
version string
|
||
author string
|
||
description string
|
||
eventLog []PluginEventLog
|
||
eventLogMu sync.RWMutex
|
||
maxEventLog int
|
||
closed bool
|
||
}
|
||
|
||
// IsClosed возвращает статус закрытия плагина
|
||
func (p *Plugin) IsClosed() bool {
|
||
p.mu.RLock()
|
||
defer p.mu.RUnlock()
|
||
return p.closed
|
||
}
|
||
|
||
// closePlugin закрывает плагин
|
||
func (p *Plugin) closePlugin() {
|
||
p.mu.Lock()
|
||
defer p.mu.Unlock()
|
||
|
||
if p.closed {
|
||
return
|
||
}
|
||
p.closed = true
|
||
if p.LState != nil {
|
||
p.LState.Close()
|
||
}
|
||
}
|
||
|
||
// ========== PluginManager ==========
|
||
|
||
// PluginManager управляет всеми загруженными плагинами
|
||
type PluginManager struct {
|
||
plugins sync.Map
|
||
logger storage.LoggerInterface
|
||
storage *storage.Storage
|
||
pluginsDir string
|
||
eventBus chan PluginEvent
|
||
enabled bool
|
||
depResolver *DependencyResolver
|
||
hotReload *HotReloadManager
|
||
marketplace *PluginMarketplace
|
||
statePool *LuaStatePool
|
||
}
|
||
|
||
// PluginEvent представляет событие от плагина
|
||
type PluginEvent struct {
|
||
PluginName string
|
||
EventType string
|
||
Data interface{}
|
||
Timestamp int64
|
||
TimestampStr string
|
||
}
|
||
|
||
// NewPluginManager создаёт новый менеджер плагинов
|
||
func NewPluginManager(pluginsDir string, logger storage.LoggerInterface, store *storage.Storage, enabled bool) *PluginManager {
|
||
pm := &PluginManager{
|
||
logger: logger,
|
||
storage: store,
|
||
pluginsDir: pluginsDir,
|
||
eventBus: make(chan PluginEvent, 1000),
|
||
enabled: enabled,
|
||
depResolver: NewDependencyResolver(),
|
||
statePool: GetLuaStatePool(),
|
||
}
|
||
|
||
if !enabled {
|
||
if logger != nil {
|
||
logger.Info("Plugin system is disabled")
|
||
}
|
||
return pm
|
||
}
|
||
|
||
if err := os.MkdirAll(pluginsDir, 0755); err != nil {
|
||
if logger != nil {
|
||
logger.Error(fmt.Sprintf("Failed to create plugins directory: %v", err))
|
||
}
|
||
}
|
||
|
||
go pm.eventLoop()
|
||
|
||
pm.hotReload = NewHotReloadManager(pm, logger)
|
||
pm.hotReload.Start()
|
||
|
||
go pm.autoLoadPlugins()
|
||
|
||
if logger != nil {
|
||
logger.Info(fmt.Sprintf("Plugin system initialized, plugins directory: %s", pluginsDir))
|
||
}
|
||
|
||
return pm
|
||
}
|
||
|
||
// NewPluginManagerFromConfig создаёт новый менеджер плагинов из конфигурации
|
||
func NewPluginManagerFromConfig(cfg *PluginConfig, logger storage.LoggerInterface, store *storage.Storage) *PluginManager {
|
||
if cfg == nil {
|
||
return NewPluginManager("plugins", logger, store, false)
|
||
}
|
||
|
||
pm := NewPluginManager(cfg.ScriptDir, logger, store, cfg.Enabled)
|
||
|
||
if cfg.Enabled && len(cfg.AllowList) > 0 {
|
||
if pm.logger != nil {
|
||
pm.logger.Info(fmt.Sprintf("Plugin allow list configured: %v", cfg.AllowList))
|
||
}
|
||
}
|
||
|
||
return pm
|
||
}
|
||
|
||
// autoLoadPlugins автоматически загружает все .lua файлы из директории плагинов
|
||
func (pm *PluginManager) autoLoadPlugins() {
|
||
if !pm.enabled {
|
||
return
|
||
}
|
||
|
||
pm.loadPluginsFromDir()
|
||
|
||
ticker := time.NewTicker(10 * time.Second)
|
||
defer ticker.Stop()
|
||
|
||
for range ticker.C {
|
||
pm.loadPluginsFromDir()
|
||
}
|
||
}
|
||
|
||
// loadPluginsFromDir загружает все плагины из директории
|
||
func (pm *PluginManager) loadPluginsFromDir() {
|
||
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))
|
||
}
|
||
return
|
||
}
|
||
|
||
for _, entry := range entries {
|
||
if entry.IsDir() {
|
||
continue
|
||
}
|
||
|
||
name := entry.Name()
|
||
if !strings.HasSuffix(name, ".lua") {
|
||
continue
|
||
}
|
||
|
||
pluginName := strings.TrimSuffix(name, ".lua")
|
||
if _, exists := pm.plugins.Load(pluginName); !exists {
|
||
pluginPath := filepath.Join(pm.pluginsDir, name)
|
||
if err := pm.LoadPluginWithSandbox(pluginName, pluginPath, SandboxLimits{
|
||
MaxCPUTime: DefaultCPULimit,
|
||
MaxMemory: DefaultMemoryLimit,
|
||
MaxExecutionTime: DefaultExecutionTimeout,
|
||
MaxInstructions: 1000000,
|
||
}); err != nil {
|
||
if pm.logger != nil {
|
||
pm.logger.Error(fmt.Sprintf("Failed to auto-load plugin %s: %v", pluginName, err))
|
||
}
|
||
} else if pm.logger != nil {
|
||
pm.logger.Info(fmt.Sprintf("Auto-loaded plugin: %s", pluginName))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// LoadPlugin загружает Lua-плагин из файла
|
||
func (pm *PluginManager) LoadPlugin(name, filePath string) error {
|
||
return pm.LoadPluginWithSandbox(name, filePath, SandboxLimits{
|
||
MaxCPUTime: DefaultCPULimit,
|
||
MaxMemory: DefaultMemoryLimit,
|
||
MaxExecutionTime: DefaultExecutionTimeout,
|
||
MaxInstructions: 1000000,
|
||
})
|
||
}
|
||
|
||
// LoadPluginWithSandbox загружает плагин с ограничениями, используя пул состояний
|
||
func (pm *PluginManager) LoadPluginWithSandbox(name, filePath string, limits SandboxLimits) error {
|
||
if !pm.enabled {
|
||
return fmt.Errorf("plugin system is disabled")
|
||
}
|
||
|
||
if pm.statePool.GetActiveCount() >= int32(MaxLuaStates) {
|
||
return fmt.Errorf("max Lua states limit reached: %d", MaxLuaStates)
|
||
}
|
||
|
||
script, err := os.ReadFile(filePath)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to read plugin file: %v", err)
|
||
}
|
||
|
||
manifest := pm.loadManifest(filePath)
|
||
|
||
if manifest != nil && len(manifest.Dependencies) > 0 {
|
||
if err := pm.checkDependencies(manifest.Dependencies); err != nil {
|
||
return fmt.Errorf("dependency check failed: %v", err)
|
||
}
|
||
}
|
||
|
||
L, err := pm.statePool.Acquire(limits)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to acquire Lua state: %v", err)
|
||
}
|
||
|
||
pm.registerDatabaseFunctions(L.LState)
|
||
pm.registerTransactionFunctions(L.LState)
|
||
pm.registerTriggerFunctions(L.LState)
|
||
pm.registerTimestampFunctions(L.LState)
|
||
|
||
done := make(chan error, 1)
|
||
go func() {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
done <- fmt.Errorf("panic during script execution: %v", r)
|
||
}
|
||
}()
|
||
done <- L.DoString(string(script))
|
||
}()
|
||
|
||
select {
|
||
case err := <-done:
|
||
if err != nil {
|
||
pm.statePool.Release(L)
|
||
return fmt.Errorf("failed to execute plugin script: %v", err)
|
||
}
|
||
case <-time.After(MaxPluginLoadTime):
|
||
pm.statePool.Release(L)
|
||
return fmt.Errorf("plugin load timeout")
|
||
}
|
||
|
||
version := pm.getPluginMetadata(L.LState, "version")
|
||
author := pm.getPluginMetadata(L.LState, "author")
|
||
description := pm.getPluginMetadata(L.LState, "description")
|
||
|
||
now := time.Now()
|
||
nowMs := now.UnixMilli()
|
||
nowStr := now.Format("2006-01-02 15:04:05.000")
|
||
|
||
var plugin interface{}
|
||
if manifest != nil {
|
||
pluginWithDeps := &PluginWithDeps{
|
||
Plugin: &Plugin{
|
||
Name: name,
|
||
FilePath: filePath,
|
||
LState: L.LState,
|
||
logger: pm.logger,
|
||
storage: pm.storage,
|
||
loadedAt: now,
|
||
loadedAtMs: nowMs,
|
||
loadedAtStr: nowStr,
|
||
version: version,
|
||
author: author,
|
||
description: description,
|
||
eventLog: make([]PluginEventLog, 0),
|
||
maxEventLog: DefaultMaxEventLogSize,
|
||
},
|
||
Manifest: manifest,
|
||
Dependencies: make(map[string]*PluginWithDeps),
|
||
Dependents: make(map[string]*PluginWithDeps),
|
||
}
|
||
pluginWithDeps.Status.Store(int32(StatusLoaded))
|
||
plugin = pluginWithDeps
|
||
|
||
pm.depResolver.RegisterPlugin(pluginWithDeps)
|
||
} else {
|
||
simplePlugin := &Plugin{
|
||
Name: name,
|
||
FilePath: filePath,
|
||
LState: L.LState,
|
||
logger: pm.logger,
|
||
storage: pm.storage,
|
||
loadedAt: now,
|
||
loadedAtMs: nowMs,
|
||
loadedAtStr: nowStr,
|
||
version: version,
|
||
author: author,
|
||
description: description,
|
||
eventLog: make([]PluginEventLog, 0),
|
||
maxEventLog: DefaultMaxEventLogSize,
|
||
}
|
||
simplePlugin.Status.Store(int32(StatusLoaded))
|
||
plugin = simplePlugin
|
||
}
|
||
|
||
pm.plugins.Store(name, plugin)
|
||
|
||
if pm.hotReload != nil {
|
||
pm.hotReload.WatchPlugin(filePath)
|
||
}
|
||
|
||
if pm.logger != nil {
|
||
pm.logger.Info(fmt.Sprintf("Plugin loaded: %s v%s by %s - %s at %s", name, version, author, description, nowStr))
|
||
}
|
||
|
||
auditTimeMs, auditTimeStr := storage.GetCurrentTimestamp()
|
||
storage.LogAudit("PLUGIN_LOAD", "PLUGIN", name, map[string]interface{}{
|
||
"version": version,
|
||
"author": author,
|
||
"description": description,
|
||
"loaded_at": nowMs,
|
||
"loaded_at_str": nowStr,
|
||
"audit_time": auditTimeMs,
|
||
"audit_time_str": auditTimeStr,
|
||
"limits_cpu_ms": limits.MaxCPUTime.Milliseconds(),
|
||
"limits_memory_mb": limits.MaxMemory / 1024 / 1024,
|
||
"active_lua_states": pm.statePool.GetActiveCount(),
|
||
})
|
||
|
||
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))
|
||
}
|
||
pm.addPluginEventLog(plugin, "on_load_error", nil, err.Error())
|
||
} else {
|
||
pm.addPluginEventLog(plugin, "on_load", nil, "")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// loadManifest загружает манифест плагина
|
||
func (pm *PluginManager) loadManifest(pluginPath string) *PluginManifest {
|
||
manifestPath := strings.TrimSuffix(pluginPath, ".lua") + ".json"
|
||
data, err := os.ReadFile(manifestPath)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
|
||
var manifest PluginManifest
|
||
if err := json.Unmarshal(data, &manifest); err != nil {
|
||
return nil
|
||
}
|
||
return &manifest
|
||
}
|
||
|
||
// checkDependencies проверяет зависимости плагина
|
||
func (pm *PluginManager) checkDependencies(deps []PluginDependency) error {
|
||
for _, dep := range deps {
|
||
if dep.Optional {
|
||
continue
|
||
}
|
||
|
||
val, ok := pm.plugins.Load(dep.Name)
|
||
if !ok {
|
||
return fmt.Errorf("missing dependency: %s", dep.Name)
|
||
}
|
||
|
||
if withDeps, ok := val.(*PluginWithDeps); ok && withDeps.Manifest != nil {
|
||
if !pm.depResolver.CheckVersionCompatibility(dep, withDeps.Manifest.Version) {
|
||
return fmt.Errorf("version incompatibility: %s requires %s but have %s",
|
||
dep.Name, dep.Version, withDeps.Manifest.Version)
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// registerTimestampFunctions регистрирует функции для работы с временными метками
|
||
func (pm *PluginManager) registerTimestampFunctions(L *lua.LState) {
|
||
L.SetGlobal("get_current_timestamp_ms", L.NewFunction(func(L *lua.LState) int {
|
||
L.Push(lua.LNumber(time.Now().UnixMilli()))
|
||
return 1
|
||
}))
|
||
|
||
L.SetGlobal("get_current_timestamp_str", L.NewFunction(func(L *lua.LState) int {
|
||
L.Push(lua.LString(time.Now().Format("2006-01-02 15:04:05.000")))
|
||
return 1
|
||
}))
|
||
|
||
L.SetGlobal("get_current_time", L.NewFunction(func(L *lua.LState) int {
|
||
now := time.Now()
|
||
table := L.NewTable()
|
||
table.RawSetString("unix_ms", lua.LNumber(now.UnixMilli()))
|
||
table.RawSetString("unix_sec", lua.LNumber(now.Unix()))
|
||
table.RawSetString("string", lua.LString(now.Format("2006-01-02 15:04:05.000")))
|
||
table.RawSetString("year", lua.LNumber(now.Year()))
|
||
table.RawSetString("month", lua.LNumber(int(now.Month())))
|
||
table.RawSetString("day", lua.LNumber(now.Day()))
|
||
table.RawSetString("hour", lua.LNumber(now.Hour()))
|
||
table.RawSetString("minute", lua.LNumber(now.Minute()))
|
||
table.RawSetString("second", lua.LNumber(now.Second()))
|
||
L.Push(table)
|
||
return 1
|
||
}))
|
||
}
|
||
|
||
// 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)
|
||
|
||
nowMs := time.Now().UnixMilli()
|
||
nowStr := time.Now().Format("2006-01-02 15:04:05.000")
|
||
event := PluginEvent{
|
||
EventType: eventType,
|
||
Data: pm.luaValueToGo(eventData),
|
||
Timestamp: nowMs,
|
||
TimestampStr: nowStr,
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
// registerTransactionFunctions регистрирует функции для работы с транзакциями
|
||
func (pm *PluginManager) registerTransactionFunctions(L *lua.LState) {
|
||
L.SetGlobal("begin_transaction", L.NewFunction(func(L *lua.LState) int {
|
||
tx := storage.BeginTransaction()
|
||
if tx == nil {
|
||
L.Push(lua.LNil)
|
||
L.Push(lua.LString("Failed to begin transaction"))
|
||
return 2
|
||
}
|
||
|
||
ud := L.NewUserData()
|
||
ud.Value = tx
|
||
L.SetMetatable(ud, L.GetTypeMetatable("transaction"))
|
||
L.Push(ud)
|
||
return 1
|
||
}))
|
||
|
||
L.SetGlobal("commit_transaction", L.NewFunction(func(L *lua.LState) int {
|
||
if err := storage.CommitCurrentTransaction(); err != nil {
|
||
L.Push(lua.LString(err.Error()))
|
||
return 1
|
||
}
|
||
L.Push(lua.LNil)
|
||
return 1
|
||
}))
|
||
|
||
L.SetGlobal("abort_transaction", L.NewFunction(func(L *lua.LState) int {
|
||
if err := storage.AbortCurrentTransaction(); err != nil {
|
||
L.Push(lua.LString(err.Error()))
|
||
return 1
|
||
}
|
||
L.Push(lua.LNil)
|
||
return 1
|
||
}))
|
||
|
||
L.SetGlobal("has_active_transaction", L.NewFunction(func(L *lua.LState) int {
|
||
L.Push(lua.LBool(storage.HasActiveTransaction()))
|
||
return 1
|
||
}))
|
||
|
||
L.SetGlobal("get_current_transaction_id", L.NewFunction(func(L *lua.LState) int {
|
||
txID := storage.GetCurrentTransactionID()
|
||
if txID == "" {
|
||
L.Push(lua.LNil)
|
||
} else {
|
||
L.Push(lua.LString(txID))
|
||
}
|
||
return 1
|
||
}))
|
||
|
||
L.SetGlobal("get_active_transactions", L.NewFunction(func(L *lua.LState) int {
|
||
txs := storage.GetActiveTransactions()
|
||
table := L.NewTable()
|
||
for i, tx := range txs {
|
||
txTable := L.NewTable()
|
||
txTable.RawSetString("id", lua.LString(tx.ID))
|
||
txTable.RawSetString("status", lua.LString(tx.Status))
|
||
txTable.RawSetString("start_time", lua.LNumber(tx.StartTime))
|
||
txTable.RawSetString("operation_count", lua.LNumber(tx.OperationCount))
|
||
table.RawSetInt(i+1, txTable)
|
||
}
|
||
L.Push(table)
|
||
return 1
|
||
}))
|
||
|
||
mt := L.NewTypeMetatable("transaction")
|
||
L.SetField(mt, "__index", L.NewFunction(func(L *lua.LState) int {
|
||
ud := L.CheckUserData(1)
|
||
tx, ok := ud.Value.(*storage.Transaction)
|
||
if !ok {
|
||
L.Push(lua.LNil)
|
||
return 1
|
||
}
|
||
method := L.CheckString(2)
|
||
|
||
switch method {
|
||
case "get_id":
|
||
L.Push(lua.LString(fmt.Sprintf("%d", tx.ID)))
|
||
case "get_operation_count":
|
||
L.Push(lua.LNumber(0))
|
||
case "get_start_time":
|
||
L.Push(lua.LNumber(tx.StartTime))
|
||
case "get_status":
|
||
status := "active"
|
||
L.Push(lua.LString(status))
|
||
default:
|
||
L.Push(lua.LNil)
|
||
}
|
||
return 1
|
||
}))
|
||
}
|
||
|
||
// registerTriggerFunctions регистрирует функции для работы с триггерами
|
||
func (pm *PluginManager) registerTriggerFunctions(L *lua.LState) {
|
||
tm := storage.GetTriggerManager()
|
||
|
||
L.SetGlobal("create_trigger", L.NewFunction(func(L *lua.LState) int {
|
||
database := L.CheckString(1)
|
||
collection := L.CheckString(2)
|
||
name := L.CheckString(3)
|
||
event := L.CheckString(4)
|
||
config := L.CheckTable(5)
|
||
|
||
configMap := make(map[string]interface{})
|
||
config.ForEach(func(key, value lua.LValue) {
|
||
if key.Type() == lua.LTString {
|
||
configMap[key.String()] = pm.luaValueToGo(value)
|
||
}
|
||
})
|
||
|
||
if err := tm.CreateTrigger(database, collection, name, storage.TriggerEvent(event), configMap); err != nil {
|
||
L.Push(lua.LString(err.Error()))
|
||
return 1
|
||
}
|
||
L.Push(lua.LNil)
|
||
return 1
|
||
}))
|
||
|
||
L.SetGlobal("drop_trigger", L.NewFunction(func(L *lua.LState) int {
|
||
collection := L.CheckString(1)
|
||
event := L.CheckString(2)
|
||
name := L.CheckString(3)
|
||
|
||
if err := tm.DropTrigger(collection, event, name); err != nil {
|
||
L.Push(lua.LString(err.Error()))
|
||
return 1
|
||
}
|
||
L.Push(lua.LNil)
|
||
return 1
|
||
}))
|
||
|
||
L.SetGlobal("enable_trigger", L.NewFunction(func(L *lua.LState) int {
|
||
collection := L.CheckString(1)
|
||
event := L.CheckString(2)
|
||
name := L.CheckString(3)
|
||
|
||
if err := tm.EnableTrigger(collection, event, name); err != nil {
|
||
L.Push(lua.LString(err.Error()))
|
||
return 1
|
||
}
|
||
L.Push(lua.LNil)
|
||
return 1
|
||
}))
|
||
|
||
L.SetGlobal("disable_trigger", L.NewFunction(func(L *lua.LState) int {
|
||
collection := L.CheckString(1)
|
||
event := L.CheckString(2)
|
||
name := L.CheckString(3)
|
||
|
||
if err := tm.DisableTrigger(collection, event, name); err != nil {
|
||
L.Push(lua.LString(err.Error()))
|
||
return 1
|
||
}
|
||
L.Push(lua.LNil)
|
||
return 1
|
||
}))
|
||
|
||
L.SetGlobal("list_triggers", L.NewFunction(func(L *lua.LState) int {
|
||
collection := L.OptString(1, "")
|
||
triggers := tm.ListTriggers(collection)
|
||
|
||
table := L.NewTable()
|
||
for i, trigger := range triggers {
|
||
triggerTable := L.NewTable()
|
||
triggerTable.RawSetString("name", lua.LString(trigger.Name))
|
||
triggerTable.RawSetString("collection", lua.LString(trigger.Collection))
|
||
triggerTable.RawSetString("event", lua.LString(string(trigger.Event)))
|
||
triggerTable.RawSetString("action", lua.LString(string(trigger.Action)))
|
||
triggerTable.RawSetString("enabled", lua.LBool(trigger.Enabled))
|
||
triggerTable.RawSetString("description", lua.LString(trigger.Description))
|
||
table.RawSetInt(i+1, triggerTable)
|
||
}
|
||
L.Push(table)
|
||
return 1
|
||
}))
|
||
}
|
||
|
||
// 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 "drop_collection":
|
||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||
name := L.CheckString(1)
|
||
err := db.DropCollection(name)
|
||
if err != nil {
|
||
L.Push(lua.LString(err.Error()))
|
||
return 1
|
||
}
|
||
L.Push(lua.LNil)
|
||
return 1
|
||
}))
|
||
case "list_collections":
|
||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||
collections := db.ListCollections()
|
||
table := L.NewTable()
|
||
for i, name := range collections {
|
||
table.RawSetInt(i+1, lua.LString(name))
|
||
}
|
||
L.Push(table)
|
||
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)
|
||
fields := make(map[string]interface{})
|
||
doc.ForEach(func(key, value lua.LValue) {
|
||
if key.Type() == lua.LTString {
|
||
fields[key.String()] = pm.luaValueToGo(value)
|
||
}
|
||
})
|
||
|
||
newDoc := storage.NewDocument()
|
||
for k, v := range fields {
|
||
newDoc.SetField(k, v)
|
||
}
|
||
|
||
err := coll.Insert(newDoc)
|
||
if err != nil {
|
||
L.Push(lua.LNil)
|
||
L.Push(lua.LString(err.Error()))
|
||
return 2
|
||
}
|
||
L.Push(lua.LString(newDoc.ID))
|
||
L.Push(lua.LNil)
|
||
return 2
|
||
}))
|
||
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
|
||
}
|
||
table := L.NewTable()
|
||
table.RawSetString("_id", lua.LString(doc.ID))
|
||
for k, v := range doc.GetFields() {
|
||
table.RawSetString(k, pm.goValueToLua(L, v))
|
||
}
|
||
L.Push(table)
|
||
return 1
|
||
}))
|
||
case "find_by_index":
|
||
L.Push(L.NewFunction(func(L *lua.LState) int {
|
||
indexName := L.CheckString(1)
|
||
value := pm.luaValueToGo(L.CheckAny(2))
|
||
|
||
docs, err := coll.FindByIndex(indexName, value)
|
||
if err != nil {
|
||
L.Push(lua.LNil)
|
||
L.Push(lua.LString(err.Error()))
|
||
return 2
|
||
}
|
||
|
||
table := L.NewTable()
|
||
for i, doc := range docs {
|
||
docTable := L.NewTable()
|
||
docTable.RawSetString("_id", lua.LString(doc.ID))
|
||
for k, v := range doc.GetFields() {
|
||
docTable.RawSetString(k, pm.goValueToLua(L, v))
|
||
}
|
||
table.RawSetInt(i+1, docTable)
|
||
}
|
||
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
|
||
}))
|
||
case "name":
|
||
L.Push(lua.LString(coll.Name()))
|
||
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 interface{}, funcName string) error {
|
||
var L *lua.LState
|
||
var p *Plugin
|
||
|
||
switch pl := plugin.(type) {
|
||
case *Plugin:
|
||
p = pl
|
||
p.mu.RLock()
|
||
L = p.LState
|
||
p.mu.RUnlock()
|
||
case *PluginWithDeps:
|
||
p = pl.Plugin
|
||
p.mu.RLock()
|
||
L = p.LState
|
||
p.mu.RUnlock()
|
||
default:
|
||
return fmt.Errorf("unknown plugin type")
|
||
}
|
||
|
||
if L == nil || p.IsClosed() {
|
||
return fmt.Errorf("plugin has no Lua state or is closed")
|
||
}
|
||
|
||
fn := L.GetGlobal(funcName)
|
||
if fn == lua.LNil {
|
||
return nil
|
||
}
|
||
|
||
var callErr error
|
||
func() {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
callErr = fmt.Errorf("panic during %s: %v", funcName, r)
|
||
}
|
||
}()
|
||
callErr = L.CallByParam(lua.P{
|
||
Fn: fn,
|
||
NRet: 0,
|
||
Protect: true,
|
||
})
|
||
}()
|
||
|
||
if callErr != nil {
|
||
return fmt.Errorf("failed to call %s: %v", funcName, callErr)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// addPluginEventLog добавляет событие в лог плагина
|
||
func (pm *PluginManager) addPluginEventLog(plugin interface{}, eventType string, data interface{}, errMsg string) {
|
||
var p *Plugin
|
||
|
||
switch pl := plugin.(type) {
|
||
case *Plugin:
|
||
p = pl
|
||
case *PluginWithDeps:
|
||
p = pl.Plugin
|
||
default:
|
||
return
|
||
}
|
||
|
||
p.eventLogMu.Lock()
|
||
defer p.eventLogMu.Unlock()
|
||
|
||
nowMs := time.Now().UnixMilli()
|
||
nowStr := time.Now().Format("2006-01-02 15:04:05.000")
|
||
eventLog := PluginEventLog{
|
||
PluginName: p.Name,
|
||
EventType: eventType,
|
||
Data: data,
|
||
Timestamp: nowMs,
|
||
TimestampStr: nowStr,
|
||
Error: errMsg,
|
||
}
|
||
|
||
p.eventLog = append(p.eventLog, eventLog)
|
||
if len(p.eventLog) > p.maxEventLog {
|
||
p.eventLog = p.eventLog[1:]
|
||
}
|
||
}
|
||
|
||
// eventLoop обрабатывает события от плагинов
|
||
func (pm *PluginManager) eventLoop() {
|
||
for event := range pm.eventBus {
|
||
if pm.logger != nil {
|
||
pm.logger.Debug(fmt.Sprintf("Plugin event [%s] at %s: %+v", event.EventType, event.TimestampStr, event.Data))
|
||
}
|
||
|
||
pm.plugins.Range(func(key, value interface{}) bool {
|
||
go pm.notifyPlugin(value, event)
|
||
return true
|
||
})
|
||
}
|
||
}
|
||
|
||
// notifyPlugin уведомляет конкретный плагин о событии
|
||
func (pm *PluginManager) notifyPlugin(plugin interface{}, event PluginEvent) {
|
||
var L *lua.LState
|
||
var name string
|
||
var p *Plugin
|
||
|
||
switch pl := plugin.(type) {
|
||
case *Plugin:
|
||
p = pl
|
||
p.mu.RLock()
|
||
L = p.LState
|
||
name = p.Name
|
||
p.mu.RUnlock()
|
||
case *PluginWithDeps:
|
||
p = pl.Plugin
|
||
p.mu.RLock()
|
||
L = p.LState
|
||
name = p.Name
|
||
p.mu.RUnlock()
|
||
default:
|
||
return
|
||
}
|
||
|
||
if L == nil || p.IsClosed() {
|
||
return
|
||
}
|
||
|
||
fn := L.GetGlobal("on_event")
|
||
if fn == lua.LNil {
|
||
return
|
||
}
|
||
|
||
event.PluginName = 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("timestamp_str", lua.LString(event.TimestampStr))
|
||
eventTable.RawSetString("data", pm.goValueToLua(L, event.Data))
|
||
|
||
var callErr error
|
||
func() {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
callErr = fmt.Errorf("panic during on_event: %v", r)
|
||
}
|
||
}()
|
||
callErr = L.CallByParam(lua.P{
|
||
Fn: fn,
|
||
NRet: 0,
|
||
Protect: true,
|
||
}, eventTable)
|
||
}()
|
||
|
||
if callErr != nil && pm.logger != nil {
|
||
pm.logger.Error(fmt.Sprintf("Plugin %s on_event error: %v", name, callErr))
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
var L *lua.LState
|
||
var p *Plugin
|
||
|
||
switch v := val.(type) {
|
||
case *Plugin:
|
||
p = v
|
||
p.mu.RLock()
|
||
L = p.LState
|
||
p.mu.RUnlock()
|
||
case *PluginWithDeps:
|
||
p = v.Plugin
|
||
p.mu.RLock()
|
||
L = p.LState
|
||
p.mu.RUnlock()
|
||
default:
|
||
return nil, fmt.Errorf("unknown plugin type")
|
||
}
|
||
|
||
if PluginStatus(p.Status.Load()) != StatusRunning {
|
||
return nil, fmt.Errorf("plugin %s is not running", pluginName)
|
||
}
|
||
|
||
if L == nil || p.IsClosed() {
|
||
return nil, fmt.Errorf("plugin %s has no Lua state or is closed", 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)
|
||
}
|
||
|
||
startTime := time.Now()
|
||
var ret lua.LValue
|
||
var callErr error
|
||
func() {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
callErr = fmt.Errorf("panic during execution: %v", r)
|
||
}
|
||
}()
|
||
callErr = L.CallByParam(lua.P{
|
||
Fn: fn,
|
||
NRet: 1,
|
||
Protect: true,
|
||
}, luaArgs...)
|
||
if callErr == nil {
|
||
ret = L.Get(-1)
|
||
L.Pop(1)
|
||
}
|
||
}()
|
||
|
||
duration := time.Since(startTime)
|
||
|
||
if callErr != nil {
|
||
pm.addPluginEventLog(val, funcName, args, callErr.Error())
|
||
return nil, fmt.Errorf("plugin execution failed: %v", callErr)
|
||
}
|
||
|
||
pm.addPluginEventLog(val, funcName, args, "")
|
||
|
||
if pm.logger != nil {
|
||
pm.logger.Debug(fmt.Sprintf("Plugin %s executed %s in %dms", pluginName, funcName, duration.Milliseconds()))
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
if err := pm.callPluginFunction(val, "on_unload"); err != nil {
|
||
if pm.logger != nil {
|
||
pm.logger.Warn(fmt.Sprintf("Plugin %s on_unload error: %v", name, err))
|
||
}
|
||
pm.addPluginEventLog(val, "on_unload_error", nil, err.Error())
|
||
} else {
|
||
pm.addPluginEventLog(val, "on_unload", nil, "")
|
||
}
|
||
|
||
var L *lua.LState
|
||
var p *Plugin
|
||
var ls *InstrumentedLState
|
||
|
||
switch v := val.(type) {
|
||
case *Plugin:
|
||
p = v
|
||
L = p.LState
|
||
p.Status.Store(int32(StatusStopped))
|
||
p.closePlugin()
|
||
if lState, ok := interface{}(L).(*InstrumentedLState); ok {
|
||
ls = lState
|
||
}
|
||
case *PluginWithDeps:
|
||
p = v.Plugin
|
||
L = p.LState
|
||
p.Status.Store(int32(StatusStopped))
|
||
p.closePlugin()
|
||
if lState, ok := interface{}(L).(*InstrumentedLState); ok {
|
||
ls = lState
|
||
}
|
||
}
|
||
|
||
if ls != nil {
|
||
pm.statePool.Release(ls)
|
||
} else if L != nil {
|
||
L.Close()
|
||
pm.statePool.active.Add(-1)
|
||
}
|
||
|
||
auditTimeMs, auditTimeStr := storage.GetCurrentTimestamp()
|
||
storage.LogAudit("PLUGIN_UNLOAD", "PLUGIN", name, map[string]interface{}{
|
||
"unloaded_at": time.Now().UnixMilli(),
|
||
"unloaded_at_str": time.Now().Format("2006-01-02 15:04:05.000"),
|
||
"audit_time": auditTimeMs,
|
||
"audit_time_str": auditTimeStr,
|
||
"active_lua_states": pm.statePool.GetActiveCount(),
|
||
})
|
||
|
||
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)
|
||
}
|
||
|
||
var p *Plugin
|
||
switch v := val.(type) {
|
||
case *Plugin:
|
||
p = v
|
||
case *PluginWithDeps:
|
||
p = v.Plugin
|
||
default:
|
||
return fmt.Errorf("unknown plugin type")
|
||
}
|
||
|
||
if p.IsClosed() {
|
||
return fmt.Errorf("plugin is closed")
|
||
}
|
||
|
||
p.Status.Store(int32(StatusRunning))
|
||
|
||
if err := pm.callPluginFunction(val, "on_start"); err != nil {
|
||
p.Status.Store(int32(StatusError))
|
||
pm.addPluginEventLog(val, "on_start_error", nil, err.Error())
|
||
return fmt.Errorf("failed to start plugin: %v", err)
|
||
}
|
||
|
||
pm.addPluginEventLog(val, "on_start", nil, "")
|
||
|
||
auditTimeMs, auditTimeStr := storage.GetCurrentTimestamp()
|
||
storage.LogAudit("PLUGIN_START", "PLUGIN", name, map[string]interface{}{
|
||
"started_at": time.Now().UnixMilli(),
|
||
"started_at_str": time.Now().Format("2006-01-02 15:04:05.000"),
|
||
"audit_time": auditTimeMs,
|
||
"audit_time_str": auditTimeStr,
|
||
"active_lua_states": pm.statePool.GetActiveCount(),
|
||
})
|
||
|
||
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)
|
||
}
|
||
|
||
if err := pm.callPluginFunction(val, "on_stop"); err != nil {
|
||
if pm.logger != nil {
|
||
pm.logger.Warn(fmt.Sprintf("Plugin %s on_stop error: %v", name, err))
|
||
}
|
||
pm.addPluginEventLog(val, "on_stop_error", nil, err.Error())
|
||
} else {
|
||
pm.addPluginEventLog(val, "on_stop", nil, "")
|
||
}
|
||
|
||
var p *Plugin
|
||
switch v := val.(type) {
|
||
case *Plugin:
|
||
p = v
|
||
case *PluginWithDeps:
|
||
p = v.Plugin
|
||
}
|
||
p.Status.Store(int32(StatusStopped))
|
||
|
||
auditTimeMs, auditTimeStr := storage.GetCurrentTimestamp()
|
||
storage.LogAudit("PLUGIN_STOP", "PLUGIN", name, map[string]interface{}{
|
||
"stopped_at": time.Now().UnixMilli(),
|
||
"stopped_at_str": time.Now().Format("2006-01-02 15:04:05.000"),
|
||
"audit_time": auditTimeMs,
|
||
"audit_time_str": auditTimeStr,
|
||
"active_lua_states": pm.statePool.GetActiveCount(),
|
||
})
|
||
|
||
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 {
|
||
switch v := value.(type) {
|
||
case *Plugin:
|
||
plugins = append(plugins, v)
|
||
case *PluginWithDeps:
|
||
plugins = append(plugins, v.Plugin)
|
||
}
|
||
return true
|
||
})
|
||
return plugins
|
||
}
|
||
|
||
// GetPlugin возвращает плагин по имени
|
||
func (pm *PluginManager) GetPlugin(name string) (*Plugin, error) {
|
||
val, ok := pm.plugins.Load(name)
|
||
if !ok {
|
||
return nil, fmt.Errorf("plugin not found: %s", name)
|
||
}
|
||
switch v := val.(type) {
|
||
case *Plugin:
|
||
return v, nil
|
||
case *PluginWithDeps:
|
||
return v.Plugin, nil
|
||
default:
|
||
return nil, fmt.Errorf("unknown plugin type")
|
||
}
|
||
}
|
||
|
||
// GetPluginWithDeps возвращает плагин с зависимостями
|
||
func (pm *PluginManager) GetPluginWithDeps(name string) (*PluginWithDeps, error) {
|
||
val, ok := pm.plugins.Load(name)
|
||
if !ok {
|
||
return nil, fmt.Errorf("plugin not found: %s", name)
|
||
}
|
||
if withDeps, ok := val.(*PluginWithDeps); ok {
|
||
return withDeps, nil
|
||
}
|
||
return nil, fmt.Errorf("plugin %s does not support dependencies", name)
|
||
}
|
||
|
||
// LoadPluginsInOrder загружает плагины в правильном порядке зависимостей
|
||
func (pm *PluginManager) LoadPluginsInOrder() error {
|
||
order, err := pm.depResolver.ResolveOrder()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
for _, plugin := range order {
|
||
if err := pm.StartPlugin(plugin.Name); err != nil {
|
||
return fmt.Errorf("failed to start plugin %s: %v", plugin.Name, err)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetHotReloadManager возвращает менеджер горячей перезагрузки
|
||
func (pm *PluginManager) GetHotReloadManager() *HotReloadManager {
|
||
return pm.hotReload
|
||
}
|
||
|
||
// GetMarketplace возвращает маркетплейс плагинов
|
||
func (pm *PluginManager) GetMarketplace() *PluginMarketplace {
|
||
return pm.marketplace
|
||
}
|
||
|
||
// EnablePluginMarketplace включает маркетплейс плагинов
|
||
func (pm *PluginManager) EnablePluginMarketplace(apiEndpoint string) {
|
||
pm.marketplace = NewPluginMarketplace(apiEndpoint, pm, pm.logger)
|
||
}
|
||
|
||
// IsEnabled возвращает статус системы плагинов
|
||
func (pm *PluginManager) IsEnabled() bool {
|
||
return pm.enabled
|
||
}
|
||
|
||
// GetPluginsDir возвращает директорию с плагинами
|
||
func (pm *PluginManager) GetPluginsDir() string {
|
||
return pm.pluginsDir
|
||
}
|
||
|
||
// GetPluginEventLog возвращает лог событий плагина
|
||
func (p *Plugin) GetPluginEventLog() []PluginEventLog {
|
||
p.eventLogMu.RLock()
|
||
defer p.eventLogMu.RUnlock()
|
||
|
||
result := make([]PluginEventLog, len(p.eventLog))
|
||
copy(result, p.eventLog)
|
||
return result
|
||
}
|
||
|
||
// ClearPluginEventLog очищает лог событий плагина
|
||
func (p *Plugin) ClearPluginEventLog() {
|
||
p.eventLogMu.Lock()
|
||
defer p.eventLogMu.Unlock()
|
||
p.eventLog = make([]PluginEventLog, 0)
|
||
}
|
||
|
||
// Version возвращает версию плагина
|
||
func (p *Plugin) Version() string {
|
||
return p.version
|
||
}
|
||
|
||
// Author возвращает автора плагина
|
||
func (p *Plugin) Author() string {
|
||
return p.author
|
||
}
|
||
|
||
// Description возвращает описание плагина
|
||
func (p *Plugin) Description() string {
|
||
return p.description
|
||
}
|
||
|
||
// LoadedAt возвращает время загрузки плагина
|
||
func (p *Plugin) LoadedAt() time.Time {
|
||
return p.loadedAt
|
||
}
|
||
|
||
// LoadedAtMs возвращает время загрузки плагина в миллисекундах
|
||
func (p *Plugin) LoadedAtMs() int64 {
|
||
return p.loadedAtMs
|
||
}
|
||
|
||
// LoadedAtStr возвращает строковое представление времени загрузки плагина
|
||
func (p *Plugin) LoadedAtStr() string {
|
||
return p.loadedAtStr
|
||
}
|
||
|
||
// GetStatus возвращает статус плагина
|
||
func (p *Plugin) GetStatus() PluginStatus {
|
||
return PluginStatus(p.Status.Load())
|
||
}
|
||
|
||
// GetLuaStatePoolStats возвращает статистику пула Lua-состояний
|
||
func (pm *PluginManager) GetLuaStatePoolStats() map[string]interface{} {
|
||
return map[string]interface{}{
|
||
"active_count": pm.statePool.GetActiveCount(),
|
||
"pool_size": pm.statePool.GetPoolSize(),
|
||
"max_allowed": MaxLuaStates,
|
||
"ttl_seconds": LuaStateTTL.Seconds(),
|
||
}
|
||
}
|
||
|
||
// Close закрывает менеджер плагинов и все ресурсы
|
||
func (pm *PluginManager) Close() error {
|
||
if pm.hotReload != nil {
|
||
pm.hotReload.Stop()
|
||
}
|
||
|
||
pm.plugins.Range(func(key, value interface{}) bool {
|
||
name := key.(string)
|
||
pm.UnloadPlugin(name)
|
||
return true
|
||
})
|
||
|
||
pm.statePool.Close()
|
||
close(pm.eventBus)
|
||
|
||
return nil
|
||
}
|