Files
futriix/internal/plugin/plugin.go

2401 lines
74 KiB
Go
Raw Normal View History

2026-06-10 23:16:57 +03:00
/*
* 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
}