/* * 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-состояний с ограничениями. // ДОБАВЛЕНО: Поддержка кастомных движков хранения через 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"` } // ========== Интерфейсы для кастомных движков ========== // CustomEngine определяет интерфейс для кастомного движка хранения type CustomEngine interface { // Базовые операции Name() string Version() string // Операции с документами Insert(doc *storage.Document) error Find(id string) (*storage.Document, error) Update(id string, updates map[string]interface{}) error Delete(id string) error // Пакетные операции BatchInsert(docs []*storage.Document) error BatchUpdate(updates map[string]map[string]interface{}) error BatchDelete(ids []string) error // Поисковые операции FindByFilter(filter func(*storage.Document) bool) ([]*storage.Document, error) FindByIndex(indexName string, value interface{}) ([]*storage.Document, error) // Индексы CreateIndex(name string, fields []string, unique bool) error DropIndex(name string) error GetIndexes() []string // Статистика и метаданные Count() int64 Size() int64 GetStats() map[string]interface{} // Жизненный цикл Initialize(config map[string]interface{}) error Close() error // События OnDocumentInserted(doc *storage.Document) OnDocumentUpdated(oldDoc, newDoc *storage.Document) OnDocumentDeleted(doc *storage.Document) } // EngineRegistry управляет зарегистрированными кастомными движками type EngineRegistry struct { engines map[string]EngineFactory activeEngines map[string]CustomEngine mu sync.RWMutex logger storage.LoggerInterface pm *PluginManager } // EngineFactory создаёт экземпляр кастомного движка type EngineFactory func(config map[string]interface{}, logger storage.LoggerInterface) (CustomEngine, error) // NewEngineRegistry создаёт новый реестр движков func NewEngineRegistry(logger storage.LoggerInterface, pm *PluginManager) *EngineRegistry { return &EngineRegistry{ engines: make(map[string]EngineFactory), activeEngines: make(map[string]CustomEngine), logger: logger, pm: pm, } } // RegisterEngine регистрирует фабрику движка func (er *EngineRegistry) RegisterEngine(name string, factory EngineFactory) { er.mu.Lock() defer er.mu.Unlock() er.engines[name] = factory if er.logger != nil { er.logger.Info(fmt.Sprintf("Engine registered: %s", name)) } } // CreateEngine создаёт экземпляр движка по имени func (er *EngineRegistry) CreateEngine(name string, config map[string]interface{}) (CustomEngine, error) { er.mu.RLock() factory, ok := er.engines[name] er.mu.RUnlock() if !ok { return nil, fmt.Errorf("engine not found: %s", name) } engine, err := factory(config, er.logger) if err != nil { return nil, err } if err := engine.Initialize(config); err != nil { return nil, err } er.mu.Lock() er.activeEngines[engine.Name()] = engine er.mu.Unlock() return engine, nil } // GetEngine возвращает активный движок по имени func (er *EngineRegistry) GetEngine(name string) CustomEngine { er.mu.RLock() defer er.mu.RUnlock() return er.activeEngines[name] } // ListEngines возвращает список зарегистрированных движков func (er *EngineRegistry) ListEngines() []string { er.mu.RLock() defer er.mu.RUnlock() engines := make([]string, 0, len(er.engines)) for name := range er.engines { engines = append(engines, name) } return engines } // ListActiveEngines возвращает список активных движков func (er *EngineRegistry) ListActiveEngines() []string { er.mu.RLock() defer er.mu.RUnlock() engines := make([]string, 0, len(er.activeEngines)) for name := range er.activeEngines { engines = append(engines, name) } return engines } // CloseAll закрывает все активные движки func (er *EngineRegistry) CloseAll() error { er.mu.Lock() defer er.mu.Unlock() var lastErr error for name, engine := range er.activeEngines { if err := engine.Close(); err != nil { lastErr = err if er.logger != nil { er.logger.Error(fmt.Sprintf("Failed to close engine %s: %v", name, err)) } } } er.activeEngines = make(map[string]CustomEngine) return lastErr } // ========== Пул 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"` EngineType string `json:"engine_type,omitempty"` // Тип движка, если плагин является движком } // 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 isEngine bool engineName string } // 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() } } // IsEnginePlugin возвращает true, если плагин является движком func (p *Plugin) IsEnginePlugin() bool { return p.isEngine } // GetEngineName возвращает имя движка, если плагин является движком func (p *Plugin) GetEngineName() string { return p.engineName } // ========== 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 engineRegistry *EngineRegistry } // 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(), } pm.engineRegistry = NewEngineRegistry(logger, pm) 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 } // GetEngineRegistry возвращает реестр движков func (pm *PluginManager) GetEngineRegistry() *EngineRegistry { return pm.engineRegistry } // RegisterEngineFromLua регистрирует движок из Lua-плагина func (pm *PluginManager) RegisterEngineFromLua(pluginName string, engineName string, factoryFn *lua.LFunction) error { if !pm.enabled { return fmt.Errorf("plugin system is disabled") } val, ok := pm.plugins.Load(pluginName) if !ok { return 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 fmt.Errorf("unknown plugin type") } if L == nil || p.IsClosed() { return fmt.Errorf("plugin has no Lua state or is closed") } // Создаём фабрику движков factory := func(config map[string]interface{}, logger storage.LoggerInterface) (CustomEngine, error) { return pm.createEngineFromLua(pluginName, engineName, factoryFn, config, logger) } pm.engineRegistry.RegisterEngine(engineName, factory) p.isEngine = true p.engineName = engineName if pm.logger != nil { pm.logger.Info(fmt.Sprintf("Engine registered from plugin %s: %s", pluginName, engineName)) } return nil } // createEngineFromLua создаёт экземпляр Lua-движка func (pm *PluginManager) createEngineFromLua(pluginName, engineName string, factoryFn *lua.LFunction, config map[string]interface{}, logger storage.LoggerInterface) (CustomEngine, error) { 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 L == nil || p.IsClosed() { return nil, fmt.Errorf("plugin has no Lua state or is closed") } // Конвертируем конфигурацию в Lua-таблицу configTable := pm.goValueToLua(L, config).(*lua.LTable) // Вызываем фабричную функцию для создания экземпляра движка if err := L.CallByParam(lua.P{ Fn: factoryFn, NRet: 1, Protect: true, }, configTable); err != nil { return nil, fmt.Errorf("failed to create engine instance: %v", err) } engineObj := L.Get(-1) L.Pop(1) if engineObj.Type() != lua.LTUserData { return nil, fmt.Errorf("factory function must return a userdata object") } // Создаём обёртку для Lua-движка engine := &LuaCustomEngine{ pluginName: pluginName, engineName: engineName, L: L, engineObj: engineObj.(*lua.LUserData), logger: logger, pm: pm, } return engine, nil } // ========== LuaCustomEngine - обёртка для Lua-движка ========== // LuaCustomEngine реализует CustomEngine для Lua-плагинов type LuaCustomEngine struct { pluginName string engineName string L *lua.LState engineObj *lua.LUserData logger storage.LoggerInterface pm *PluginManager mu sync.RWMutex initialized bool closed bool } func (e *LuaCustomEngine) Name() string { return e.engineName } func (e *LuaCustomEngine) Version() string { return e.callStringMethod("version", nil) } func (e *LuaCustomEngine) Initialize(config map[string]interface{}) error { e.mu.Lock() defer e.mu.Unlock() configTable := e.pm.goValueToLua(e.L, config).(*lua.LTable) err := e.callMethod("initialize", configTable) if err != nil { return err } e.initialized = true return nil } func (e *LuaCustomEngine) Insert(doc *storage.Document) error { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return fmt.Errorf("engine not initialized or closed") } docTable := e.pm.goValueToLua(e.L, doc.ToMap()).(*lua.LTable) return e.callMethod("insert", docTable) } func (e *LuaCustomEngine) Find(id string) (*storage.Document, error) { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return nil, fmt.Errorf("engine not initialized or closed") } result := e.callMethodWithReturn("find", lua.LString(id)) if result == nil || result == lua.LNil { return nil, fmt.Errorf("document not found: %s", id) } if table, ok := result.(*lua.LTable); ok { return e.tableToDocument(table), nil } return nil, fmt.Errorf("invalid return type from find") } func (e *LuaCustomEngine) Update(id string, updates map[string]interface{}) error { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return fmt.Errorf("engine not initialized or closed") } updatesTable := e.pm.goValueToLua(e.L, updates).(*lua.LTable) return e.callMethod("update", lua.LString(id), updatesTable) } func (e *LuaCustomEngine) Delete(id string) error { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return fmt.Errorf("engine not initialized or closed") } return e.callMethod("delete", lua.LString(id)) } func (e *LuaCustomEngine) BatchInsert(docs []*storage.Document) error { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return fmt.Errorf("engine not initialized or closed") } docsTable := e.L.NewTable() for i, doc := range docs { docTable := e.pm.goValueToLua(e.L, doc.ToMap()).(*lua.LTable) docsTable.RawSetInt(i+1, docTable) } return e.callMethod("batch_insert", docsTable) } func (e *LuaCustomEngine) BatchUpdate(updates map[string]map[string]interface{}) error { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return fmt.Errorf("engine not initialized or closed") } updatesTable := e.pm.goValueToLua(e.L, updates).(*lua.LTable) return e.callMethod("batch_update", updatesTable) } func (e *LuaCustomEngine) BatchDelete(ids []string) error { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return fmt.Errorf("engine not initialized or closed") } idsTable := e.L.NewTable() for i, id := range ids { idsTable.RawSetInt(i+1, lua.LString(id)) } return e.callMethod("batch_delete", idsTable) } func (e *LuaCustomEngine) FindByFilter(filter func(*storage.Document) bool) ([]*storage.Document, error) { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return nil, fmt.Errorf("engine not initialized or closed") } // Фильтр не может быть передан в Lua напрямую, поэтому возвращаем все документы // и фильтруем в Go, либо реализуем через Lua-функцию result := e.callMethodWithReturn("find_all", nil) if result == nil || result == lua.LNil { return []*storage.Document{}, nil } docs := make([]*storage.Document, 0) if table, ok := result.(*lua.LTable); ok { table.ForEach(func(key, value lua.LValue) { if docTable, ok := value.(*lua.LTable); ok { doc := e.tableToDocument(docTable) if filter(doc) { docs = append(docs, doc) } } }) } return docs, nil } func (e *LuaCustomEngine) FindByIndex(indexName string, value interface{}) ([]*storage.Document, error) { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return nil, fmt.Errorf("engine not initialized or closed") } luaValue := e.pm.goValueToLua(e.L, value) result := e.callMethodWithReturn("find_by_index", lua.LString(indexName), luaValue) if result == nil || result == lua.LNil { return []*storage.Document{}, nil } docs := make([]*storage.Document, 0) if table, ok := result.(*lua.LTable); ok { table.ForEach(func(key, value lua.LValue) { if docTable, ok := value.(*lua.LTable); ok { docs = append(docs, e.tableToDocument(docTable)) } }) } return docs, nil } func (e *LuaCustomEngine) CreateIndex(name string, fields []string, unique bool) error { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return fmt.Errorf("engine not initialized or closed") } fieldsTable := e.L.NewTable() for i, field := range fields { fieldsTable.RawSetInt(i+1, lua.LString(field)) } return e.callMethod("create_index", lua.LString(name), fieldsTable, lua.LBool(unique)) } func (e *LuaCustomEngine) DropIndex(name string) error { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return fmt.Errorf("engine not initialized or closed") } return e.callMethod("drop_index", lua.LString(name)) } func (e *LuaCustomEngine) GetIndexes() []string { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return []string{} } result := e.callMethodWithReturn("get_indexes", nil) if result == nil || result == lua.LNil { return []string{} } indexes := make([]string, 0) if table, ok := result.(*lua.LTable); ok { table.ForEach(func(key, value lua.LValue) { if str, ok := value.(lua.LString); ok { indexes = append(indexes, string(str)) } }) } return indexes } func (e *LuaCustomEngine) Count() int64 { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return 0 } result := e.callMethodWithReturn("count", nil) if result == nil || result == lua.LNil { return 0 } if num, ok := result.(lua.LNumber); ok { return int64(num) } return 0 } func (e *LuaCustomEngine) Size() int64 { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return 0 } result := e.callMethodWithReturn("size", nil) if result == nil || result == lua.LNil { return 0 } if num, ok := result.(lua.LNumber); ok { return int64(num) } return 0 } func (e *LuaCustomEngine) GetStats() map[string]interface{} { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return make(map[string]interface{}) } result := e.callMethodWithReturn("get_stats", nil) if result == nil || result == lua.LNil { return make(map[string]interface{}) } if table, ok := result.(*lua.LTable); ok { return e.pm.luaTableToMap(table) } return make(map[string]interface{}) } func (e *LuaCustomEngine) Close() error { e.mu.Lock() defer e.mu.Unlock() if e.closed { return nil } if e.initialized { _ = e.callMethod("close", nil) } e.closed = true return nil } func (e *LuaCustomEngine) OnDocumentInserted(doc *storage.Document) { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return } docTable := e.pm.goValueToLua(e.L, doc.ToMap()).(*lua.LTable) _ = e.callMethod("on_document_inserted", docTable) } func (e *LuaCustomEngine) OnDocumentUpdated(oldDoc, newDoc *storage.Document) { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return } oldTable := e.pm.goValueToLua(e.L, oldDoc.ToMap()).(*lua.LTable) newTable := e.pm.goValueToLua(e.L, newDoc.ToMap()).(*lua.LTable) _ = e.callMethod("on_document_updated", oldTable, newTable) } func (e *LuaCustomEngine) OnDocumentDeleted(doc *storage.Document) { e.mu.RLock() defer e.mu.RUnlock() if !e.initialized || e.closed { return } docTable := e.pm.goValueToLua(e.L, doc.ToMap()).(*lua.LTable) _ = e.callMethod("on_document_deleted", docTable) } // Вспомогательные методы для LuaCustomEngine func (e *LuaCustomEngine) callMethod(methodName string, args ...lua.LValue) error { method := e.L.GetField(e.engineObj, methodName) if method.Type() != lua.LTFunction { return fmt.Errorf("method %s not found in engine", methodName) } callArgs := make([]lua.LValue, 0, len(args)+1) callArgs = append(callArgs, e.engineObj) callArgs = append(callArgs, args...) if err := e.L.CallByParam(lua.P{ Fn: method, NRet: 0, Protect: true, }, callArgs...); err != nil { return fmt.Errorf("failed to call method %s: %v", methodName, err) } return nil } func (e *LuaCustomEngine) callMethodWithReturn(methodName string, args ...lua.LValue) lua.LValue { method := e.L.GetField(e.engineObj, methodName) if method.Type() != lua.LTFunction { return lua.LNil } callArgs := make([]lua.LValue, 0, len(args)+1) callArgs = append(callArgs, e.engineObj) callArgs = append(callArgs, args...) if err := e.L.CallByParam(lua.P{ Fn: method, NRet: 1, Protect: true, }, callArgs...); err != nil { if e.logger != nil { e.logger.Error(fmt.Sprintf("Failed to call method %s: %v", methodName, err)) } return lua.LNil } result := e.L.Get(-1) e.L.Pop(1) return result } func (e *LuaCustomEngine) callStringMethod(methodName string, args ...lua.LValue) string { result := e.callMethodWithReturn(methodName, args...) if result == nil || result == lua.LNil { return "" } if str, ok := result.(lua.LString); ok { return string(str) } return "" } func (e *LuaCustomEngine) tableToDocument(table *lua.LTable) *storage.Document { doc := storage.NewDocument() table.ForEach(func(key, value lua.LValue) { keyStr := key.String() // Пропускаем служебные поля с префиксом _ if strings.HasPrefix(keyStr, "_") { return } goValue := e.pm.luaValueToGo(value) doc.SetField(keyStr, goValue) }) return doc } // 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) pm.registerEngineFunctions(L.LState, name) 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") isEngine := pm.getPluginMetadata(L.LState, "engine_type") != "" engineName := pm.getPluginMetadata(L.LState, "engine_type") 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, isEngine: isEngine, engineName: engineName, }, 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, isEngine: isEngine, engineName: engineName, } simplePlugin.Status.Store(int32(StatusLoaded)) plugin = simplePlugin } pm.plugins.Store(name, plugin) if pm.hotReload != nil { pm.hotReload.WatchPlugin(filePath) } if pm.logger != nil { engineInfo := "" if isEngine { engineInfo = fmt.Sprintf(" [ENGINE: %s]", engineName) } pm.logger.Info(fmt.Sprintf("Plugin loaded: %s v%s by %s - %s%s at %s", name, version, author, description, engineInfo, 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(), "is_engine": isEngine, "engine_type": engineName, }) 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, "") } // Если плагин является движком, регистрируем его if isEngine && engineName != "" { if err := pm.registerEngineFromPlugin(plugin, engineName); err != nil { if pm.logger != nil { pm.logger.Warn(fmt.Sprintf("Failed to register engine from plugin %s: %v", name, err)) } } } return nil } // registerEngineFromPlugin регистрирует движок из загруженного плагина func (pm *PluginManager) registerEngineFromPlugin(plugin interface{}, engineName string) error { var L *lua.LState var p *Plugin switch v := plugin.(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 fmt.Errorf("unknown plugin type") } if L == nil { return fmt.Errorf("plugin has no Lua state") } // Получаем фабричную функцию движка factoryFn := L.GetGlobal("create_engine") if factoryFn.Type() != lua.LTFunction { // Пробуем альтернативное имя factoryFn = L.GetGlobal("new_engine") if factoryFn.Type() != lua.LTFunction { return fmt.Errorf("engine plugin must export create_engine or new_engine function") } } return pm.RegisterEngineFromLua(p.Name, engineName, factoryFn.(*lua.LFunction)) } // 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) } // registerEngineFunctions регистрирует функции для создания кастомных движков func (pm *PluginManager) registerEngineFunctions(L *lua.LState, pluginName string) { L.SetGlobal("register_engine", L.NewFunction(func(L *lua.LState) int { engineName := L.CheckString(1) factoryFn := L.CheckFunction(2) if err := pm.RegisterEngineFromLua(pluginName, engineName, factoryFn); err != nil { L.Push(lua.LString(err.Error())) return 1 } L.Push(lua.LNil) return 1 })) L.SetGlobal("get_engine", L.NewFunction(func(L *lua.LState) int { engineName := L.CheckString(1) engine := pm.engineRegistry.GetEngine(engineName) if engine == nil { L.Push(lua.LNil) L.Push(lua.LString(fmt.Sprintf("engine not found: %s", engineName))) return 2 } ud := L.NewUserData() ud.Value = engine L.SetMetatable(ud, L.GetTypeMetatable("custom_engine")) L.Push(ud) return 1 })) L.SetGlobal("list_engines", L.NewFunction(func(L *lua.LState) int { engines := pm.engineRegistry.ListEngines() table := L.NewTable() for i, name := range engines { table.RawSetInt(i+1, lua.LString(name)) } L.Push(table) return 1 })) L.SetGlobal("list_active_engines", L.NewFunction(func(L *lua.LState) int { engines := pm.engineRegistry.ListActiveEngines() table := L.NewTable() for i, name := range engines { table.RawSetInt(i+1, lua.LString(name)) } L.Push(table) return 1 })) // Настройка метатаблицы для кастомного движка engineMt := L.NewTypeMetatable("custom_engine") L.SetField(engineMt, "__index", L.NewFunction(func(L *lua.LState) int { engine := L.CheckUserData(1).Value.(CustomEngine) method := L.CheckString(2) switch method { case "name": L.Push(lua.LString(engine.Name())) case "version": L.Push(lua.LString(engine.Version())) case "count": L.Push(L.NewFunction(func(L *lua.LState) int { L.Push(lua.LNumber(engine.Count())) return 1 })) case "size": L.Push(L.NewFunction(func(L *lua.LState) int { L.Push(lua.LNumber(engine.Size())) return 1 })) case "insert": L.Push(L.NewFunction(func(L *lua.LState) int { docData := L.CheckTable(1) doc := storage.NewDocument() docData.ForEach(func(key, value lua.LValue) { if key.Type() == lua.LTString { doc.SetField(key.String(), pm.luaValueToGo(value)) } }) if err := engine.Insert(doc); err != nil { L.Push(lua.LString(err.Error())) return 1 } L.Push(lua.LNil) return 1 })) case "find": L.Push(L.NewFunction(func(L *lua.LState) int { id := L.CheckString(1) doc, err := engine.Find(id) if err != nil { L.Push(lua.LNil) L.Push(lua.LString(err.Error())) return 2 } resultTable := pm.goValueToLua(L, doc.ToMap()).(*lua.LTable) L.Push(resultTable) return 1 })) case "update": L.Push(L.NewFunction(func(L *lua.LState) int { id := L.CheckString(1) updates := L.CheckTable(2) updatesMap := pm.luaTableToMap(updates) if err := engine.Update(id, updatesMap); 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) if err := engine.Delete(id); err != nil { L.Push(lua.LString(err.Error())) return 1 } L.Push(lua.LNil) return 1 })) case "create_index": L.Push(L.NewFunction(func(L *lua.LState) int { name := L.CheckString(1) fieldsTable := L.CheckTable(2) unique := L.OptBool(3, false) fields := make([]string, 0) fieldsTable.ForEach(func(key, value lua.LValue) { if str, ok := value.(lua.LString); ok { fields = append(fields, string(str)) } }) if err := engine.CreateIndex(name, fields, unique); err != nil { L.Push(lua.LString(err.Error())) return 1 } L.Push(lua.LNil) return 1 })) case "drop_index": L.Push(L.NewFunction(func(L *lua.LState) int { name := L.CheckString(1) if err := engine.DropIndex(name); err != nil { L.Push(lua.LString(err.Error())) return 1 } L.Push(lua.LNil) return 1 })) case "get_stats": L.Push(L.NewFunction(func(L *lua.LState) int { stats := engine.GetStats() statsTable := pm.goValueToLua(L, stats).(*lua.LTable) L.Push(statsTable) return 1 })) default: L.Push(lua.LNil) } return 1 })) } // 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: return pm.luaTableToMap(v) default: return v.String() } } // luaTableToMap конвертирует Lua-таблицу в Go-карту func (pm *PluginManager) luaTableToMap(table *lua.LTable) map[string]interface{} { result := make(map[string]interface{}) table.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 } // 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 "" } // 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() } if err := pm.engineRegistry.CloseAll(); err != nil { if pm.logger != nil { pm.logger.Error(fmt.Sprintf("Error closing engines: %v", err)) } } pm.plugins.Range(func(key, value interface{}) bool { name := key.(string) pm.UnloadPlugin(name) return true }) pm.statePool.Close() close(pm.eventBus) return nil }