/* * 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 }