package aastro import ( "errors" "fmt" "plugin" "slices" "strings" "go.uber.org/zap" "github.com/voidrunner3074/aastro/sdk" ) const ( sourceBuiltin = "builtin" sourceFile = "file" ) const ( builtinPluginsPath = "/usr/local/lib/aastro/plugins/" builtinMiddlewaresPath = "/usr/local/lib/aastro/middlewares/" ) const extSo = ".so" func initPlugins(cfgs []PluginConfig, log *zap.Logger) ([]sdk.Plugin, error) { plugins := make([]sdk.Plugin, 0, len(cfgs)) for _, cfg := range cfgs { if slices.ContainsFunc(plugins, func(p sdk.Plugin) bool { return p.Info().Name == cfg.Name }) { continue } soPath, err := resolveSoPath(cfg.Source, cfg.Name, cfg.Path, builtinPluginsPath) if err != nil { return nil, fmt.Errorf("resolve .so path: %w", err) } plugin, err := loadPlugin(soPath, cfg.Config, log) if err != nil { if cfg.Source == sourceBuiltin { return nil, fmt.Errorf("cannot load builtin plugin %q: %w", cfg.Name, err) } return nil, fmt.Errorf("cannot load plugin %q from path %q: %w", cfg.Name, cfg.Path, err) } log.Info("plugin initialized", zap.String("name", plugin.Info().Name)) plugins = append(plugins, plugin) } return plugins, nil } func initMiddlewares(cfgs []MiddlewareConfig, log *zap.Logger) ([]sdk.Middleware, error) { middlewares := make([]sdk.Middleware, 0, len(cfgs)) for _, cfg := range cfgs { if slices.ContainsFunc(middlewares, func(m sdk.Middleware) bool { return m.Name() == cfg.Name }) { continue } soPath, err := resolveSoPath(cfg.Source, cfg.Name, cfg.Path, builtinMiddlewaresPath) if err != nil { return nil, fmt.Errorf("resolve .so path: %w", err) } middleware, err := loadMiddleware(soPath, cfg.Config, log) if err != nil { if cfg.Source == sourceBuiltin { return nil, fmt.Errorf("cannot load builtin middleware %q: %w", cfg.Name, err) } return nil, fmt.Errorf("cannot load middleware %q from path %q: %w", cfg.Name, cfg.Path, err) } log.Info("middleware initialized", zap.String("name", middleware.Name())) middlewares = append(middlewares, middleware) } return middlewares, nil } func resolveSoPath(source, name, filePath, builtinPath string) (string, error) { switch source { case sourceBuiltin: return builtinPath + name + extSo, nil case sourceFile: base := filePath if !strings.HasSuffix(base, "/") { base += "/" } return base + name + extSo, nil default: return "", fmt.Errorf("invalid plugin source %q", source) } } func loadPlugin(path string, cfg map[string]interface{}, log *zap.Logger) (sdk.Plugin, error) { factory, err := loadSymbol[func() sdk.Plugin](path, "NewPlugin", log) if err != nil { return nil, fmt.Errorf("load plugin symbol: %w", err) } p := factory() if p == nil { return nil, fmt.Errorf("plugin factory for path %q returned nil", path) } if err = p.Init(cfg); err != nil { return nil, fmt.Errorf("init plugin %s: %w", p.Info().Name, err) } return p, nil } func loadMiddleware(path string, cfg map[string]interface{}, log *zap.Logger) (sdk.Middleware, error) { factory, err := loadSymbol[func() sdk.Middleware](path, "NewMiddleware", log) if err != nil { return nil, fmt.Errorf("load middleware symbol: %w", err) } mw := factory() if mw == nil { return nil, fmt.Errorf("middleware factory for path %q returned nil", path) } if err = mw.Init(cfg); err != nil { return nil, fmt.Errorf("init middleware %s: %w", mw.Name(), err) } return mw, nil } func loadSymbol[T any](path, symbol string, log *zap.Logger) (T, error) { var zero T log = log.With(zap.String("path", path)) p, err := plugin.Open(path) if err != nil { return zero, fmt.Errorf("open plugin: %w", err) } sym, err := p.Lookup(symbol) if err != nil { return zero, fmt.Errorf("lookup symbol: %w", err) } factory, ok := sym.(T) if !ok { return zero, errors.New("symbol has wrong signature") } pl := factory log.Debug("symbol loaded successfully") return pl, nil }