3349 lines
102 KiB
Go
3349 lines
102 KiB
Go
|
|
/*
|
|||
|
|
* Copyright 2026 Safronov Grigorii
|
|||
|
|
*
|
|||
|
|
* Licensed under the CDDL, Version 1.0 (the "License");
|
|||
|
|
* you may not use this file except in compliance with the License.
|
|||
|
|
*
|
|||
|
|
* You may obtain a copy of the License at
|
|||
|
|
* https://opensource.org/licenses/CDDL-1.0
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
// Файл: internal/plugin/plugin.go
|
|||
|
|
// Назначение: Система плагинов на основе Lua для расширения функциональности СУБД.
|
|||
|
|
// Позволяет загружать Lua-скрипты как плагины, выполнять их в изолированном окружении,
|
|||
|
|
// взаимодействовать с данными СУБД и логировать действия плагинов в общий лог-файл.
|
|||
|
|
// УЛУЧШЕНИЯ: Sandbox с CPU/memory limits, Plugin Dependencies, Hot Reload (без fsnotify),
|
|||
|
|
// Plugin Marketplace API, Пул Lua-состояний с ограничениями.
|
|||
|
|
// ДОБАВЛЕНО: Поддержка кастомных движков хранения через 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
|
|||
|
|
}
|