Files
futriix/internal/plugin/777plugin.go

2401 lines
74 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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
}