diff --git a/cmd/plugins.go b/cmd/plugins.go new file mode 100644 index 00000000..ffee9775 --- /dev/null +++ b/cmd/plugins.go @@ -0,0 +1,49 @@ +package cmd + +import ( + "encoding/json" + "os" + + "github.com/demodesk/neko/internal/config" + "github.com/demodesk/neko/internal/plugins" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +func init() { + command := &cobra.Command{ + Use: "plugins [directory]", + Short: "load, verify and list plugins", + Long: `load, verify and list plugins`, + Run: pluginsCmd, + Args: cobra.MaximumNArgs(1), + } + root.AddCommand(command) +} + +func pluginsCmd(cmd *cobra.Command, args []string) { + pluginDir := "/etc/neko/plugins" + if len(args) > 0 { + pluginDir = args[0] + } + log.Info().Str("dir", pluginDir).Msg("plugins directory") + + plugs := plugins.New(&config.Plugins{ + Enabled: true, + Required: true, + Dir: pluginDir, + }) + + meta := plugs.Metadata() + if len(meta) == 0 { + log.Fatal().Msg("no plugins found") + } + + // marshal indent to stdout + dec := json.NewEncoder(os.Stdout) + dec.SetIndent("", " ") + err := dec.Encode(meta) + if err != nil { + log.Fatal().Err(err).Msg("unable to marshal metadata") + } +} diff --git a/cmd/serve.go b/cmd/serve.go index 63e1579f..d709a523 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -24,14 +24,13 @@ func init() { service := serve{} command := &cobra.Command{ - Use: "serve", - Short: "serve neko streaming server", - Long: `serve neko streaming server`, - Run: service.Command, + Use: "serve", + Short: "serve neko streaming server", + Long: `serve neko streaming server`, + PreRun: service.PreRun, + Run: service.Run, } - cobra.OnInitialize(service.Preflight) - if err := service.Init(command); err != nil { log.Panic().Err(err).Msg("unable to initialize configuration") } @@ -91,7 +90,7 @@ func (c *serve) Init(cmd *cobra.Command) error { return nil } -func (c *serve) Preflight() { +func (c *serve) PreRun(cmd *cobra.Command, args []string) { c.logger = log.With().Str("service", "neko").Logger() c.configs.Desktop.Set() @@ -198,7 +197,7 @@ func (c *serve) Shutdown() { c.logger.Err(err).Msg("member manager disconnect") } -func (c *serve) Command(cmd *cobra.Command, args []string) { +func (c *serve) Run(cmd *cobra.Command, args []string) { c.logger.Info().Msg("starting neko server") c.Start(cmd) c.logger.Info().Msg("neko ready") diff --git a/internal/config/plugins.go b/internal/config/plugins.go index 3ca005be..2c3544c1 100644 --- a/internal/config/plugins.go +++ b/internal/config/plugins.go @@ -6,8 +6,9 @@ import ( ) type Plugins struct { - Enabled bool - Dir string + Enabled bool + Dir string + Required bool } func (Plugins) Init(cmd *cobra.Command) error { @@ -21,10 +22,16 @@ func (Plugins) Init(cmd *cobra.Command) error { return err } + cmd.PersistentFlags().Bool("plugins.required", false, "if true, neko will exit if there is an error when loading a plugin") + if err := viper.BindPFlag("plugins.required", cmd.PersistentFlags().Lookup("plugins.required")); err != nil { + return err + } + return nil } func (s *Plugins) Set() { s.Enabled = viper.GetBool("plugins.enabled") s.Dir = viper.GetString("plugins.dir") + s.Required = viper.GetBool("plugins.required") } diff --git a/internal/plugins/dependency.go b/internal/plugins/dependency.go index b9f19388..d31b2919 100644 --- a/internal/plugins/dependency.go +++ b/internal/plugins/dependency.go @@ -15,88 +15,113 @@ type dependency struct { logger zerolog.Logger } +func (a *dependency) findPlugin(name string) (*dependency, bool) { + if a == nil { + return nil, false + } + + if a.plugin.Name() == name { + return a, true + } + + for _, dep := range a.dependsOn { + plug, ok := dep.findPlugin(name) + if ok { + return plug, true + } + } + + return nil, false +} + +func (a *dependency) startPlugin(pm types.PluginManagers) error { + if a.invoked { + return nil + } + + a.invoked = true + + for _, do := range a.dependsOn { + if err := do.startPlugin(pm); err != nil { + return fmt.Errorf("plugin's '%s' dependency: %w", a.plugin.Name(), err) + } + } + + err := a.plugin.Start(pm) + if err != nil { + return fmt.Errorf("plugin '%s' failed to start: %w", a.plugin.Name(), err) + } + + a.logger.Info().Str("plugin", a.plugin.Name()).Msg("plugin started") + return nil +} + type dependiencies struct { deps map[string]*dependency logger zerolog.Logger } func (d *dependiencies) addPlugin(plugin types.Plugin) error { - plug, ok := d.deps[plugin.Name()] + pluginName := plugin.Name() + + plug, ok := d.deps[pluginName] if !ok { plug = &dependency{} } else if plug.plugin != nil { - return fmt.Errorf("plugin '%s' already added", plugin.Name()) + return fmt.Errorf("plugin '%s' already added", pluginName) } plug.plugin = plugin plug.logger = d.logger - d.deps[plugin.Name()] = plug + d.deps[pluginName] = plug dplug, ok := plugin.(types.DependablePlugin) if !ok { return nil } - for _, dep := range dplug.DependsOn() { - var dependsOn *dependency - dependsOn, ok = d.deps[dep] + for _, depName := range dplug.DependsOn() { + dependsOn, ok := d.deps[depName] if !ok { dependsOn = &dependency{} } else if dependsOn.plugin != nil { // if there is a cyclical dependency, break it and return error - if tdep := dependsOn.findPlugin(plugin.Name()); tdep != nil { + if tdep, ok := dependsOn.findPlugin(pluginName); ok { dependsOn.dependsOn = nil - delete(d.deps, plugin.Name()) - return fmt.Errorf("cyclical dependency detected: '%s' <-> '%s'", plugin.Name(), tdep.plugin.Name()) + delete(d.deps, pluginName) + return fmt.Errorf("cyclical dependency detected: '%s' <-> '%s'", pluginName, tdep.plugin.Name()) } } plug.dependsOn = append(plug.dependsOn, dependsOn) - d.deps[dep] = dependsOn + d.deps[depName] = dependsOn } return nil } +func (d *dependiencies) findPlugin(name string) (*dependency, bool) { + for _, dep := range d.deps { + plug, ok := dep.findPlugin(name) + if ok { + return plug, true + } + } + return nil, false +} + func (d *dependiencies) start(pm types.PluginManagers) error { - for _, p := range d.deps { - if err := p.start(pm); err != nil { + for _, dep := range d.deps { + if err := dep.startPlugin(pm); err != nil { return err } } return nil } -func (d *dependiencies) findPlugin(name string) (*dependency, bool) { - for _, p := range d.deps { - if found := p.findPlugin(name); found != nil { - return found, true - } - } - return nil, false -} - -func (a *dependency) findPlugin(name string) *dependency { - if a == nil { - return nil - } - - if a.plugin.Name() == name { - return a - } - - for _, p := range a.dependsOn { - if found := p.findPlugin(name); found != nil { - return found - } - } - - return nil -} - func (d *dependiencies) forEach(f func(*dependency) error) error { - for _, dp := range d.deps { - if err := f(dp); err != nil { + for _, dep := range d.deps { + if err := f(dep); err != nil { return err } } @@ -106,25 +131,3 @@ func (d *dependiencies) forEach(f func(*dependency) error) error { func (d *dependiencies) len() int { return len(d.deps) } - -func (a *dependency) start(pm types.PluginManagers) error { - if a.invoked { - return nil - } - - a.invoked = true - - for _, do := range a.dependsOn { - if err := do.start(pm); err != nil { - return err - } - } - - err := a.plugin.Start(pm) - a.logger.Err(err).Str("plugin", a.plugin.Name()).Msg("plugin start") - if err != nil { - return fmt.Errorf("plugin %s failed to start: %s", a.plugin.Name(), err) - } - - return nil -} diff --git a/internal/plugins/manager.go b/internal/plugins/manager.go index 027d9314..79d8cc28 100644 --- a/internal/plugins/manager.go +++ b/internal/plugins/manager.go @@ -16,12 +16,14 @@ import ( type ManagerCtx struct { logger zerolog.Logger + config *config.Plugins plugins dependiencies } func New(config *config.Plugins) *ManagerCtx { manager := &ManagerCtx{ logger: log.With().Str("module", "plugins").Logger(), + config: config, plugins: dependiencies{ deps: make(map[string]*dependency), }, @@ -31,7 +33,13 @@ func New(config *config.Plugins) *ManagerCtx { if config.Enabled { err := manager.loadDir(config.Dir) - manager.logger.Err(err).Msgf("loading finished, total %d plugins", manager.plugins.len()) + + // only log error if plugin is not required + if err != nil && config.Required { + manager.logger.Fatal().Err(err).Msg("error loading plugins") + } + + manager.logger.Info().Msgf("loading finished, total %d plugins", manager.plugins.len()) } return manager @@ -48,6 +56,13 @@ func (manager *ManagerCtx) loadDir(dir string) error { } err = manager.load(path) + + // return error if plugin is required + if err != nil && manager.config.Required { + return err + } + + // otherwise only log error if plugin is not required manager.logger.Err(err).Str("plugin", path).Msg("loading a plugin") return nil }) @@ -70,25 +85,24 @@ func (manager *ManagerCtx) load(path string) error { } if err = manager.plugins.addPlugin(p); err != nil { - return fmt.Errorf("failed to add plugin '%s': %w", p.Name(), err) + return fmt.Errorf("failed to add plugin: %w", err) } - manager.logger.Info().Msgf("loaded plugin '%s', total %d plugins", p.Name(), manager.plugins.len()) return nil } func (manager *ManagerCtx) InitConfigs(cmd *cobra.Command) { - _ = manager.plugins.forEach(func(plug *dependency) error { - if err := plug.plugin.Config().Init(cmd); err != nil { - log.Err(err).Str("plugin", plug.plugin.Name()).Msg("unable to initialize configuration") + _ = manager.plugins.forEach(func(d *dependency) error { + if err := d.plugin.Config().Init(cmd); err != nil { + log.Err(err).Str("plugin", d.plugin.Name()).Msg("unable to initialize configuration") } return nil }) } func (manager *ManagerCtx) SetConfigs() { - _ = manager.plugins.forEach(func(plug *dependency) error { - plug.plugin.Config().Set() + _ = manager.plugins.forEach(func(d *dependency) error { + d.plugin.Config().Set() return nil }) } @@ -98,18 +112,26 @@ func (manager *ManagerCtx) Start( webSocketManager types.WebSocketManager, apiManager types.ApiManager, ) { - _ = manager.plugins.start(types.PluginManagers{ + err := manager.plugins.start(types.PluginManagers{ SessionManager: sessionManager, WebSocketManager: webSocketManager, ApiManager: apiManager, LoadServiceFromPlugin: manager.LookupService, }) + + if err != nil { + if manager.config.Required { + manager.logger.Fatal().Err(err).Msg("failed to start plugins, exiting...") + } else { + manager.logger.Err(err).Msg("failed to start plugins, skipping...") + } + } } func (manager *ManagerCtx) Shutdown() error { - _ = manager.plugins.forEach(func(plug *dependency) error { - err := plug.plugin.Shutdown() - manager.logger.Err(err).Str("plugin", plug.plugin.Name()).Msg("plugin shutdown") + _ = manager.plugins.forEach(func(d *dependency) error { + err := d.plugin.Shutdown() + manager.logger.Err(err).Str("plugin", d.plugin.Name()).Msg("plugin shutdown") return nil }) return nil @@ -128,3 +150,28 @@ func (manager *ManagerCtx) LookupService(pluginName string) (any, error) { return expPlug.ExposeService(), nil } + +func (manager *ManagerCtx) Metadata() []types.PluginMetadata { + var plugins []types.PluginMetadata + + _ = manager.plugins.forEach(func(d *dependency) error { + dependsOn := make([]string, 0) + deps, isDependalbe := d.plugin.(types.DependablePlugin) + if isDependalbe { + dependsOn = deps.DependsOn() + } + + _, isExposable := d.plugin.(types.ExposablePlugin) + + plugins = append(plugins, types.PluginMetadata{ + Name: d.plugin.Name(), + IsDependable: isDependalbe, + IsExposable: isExposable, + DependsOn: dependsOn, + }) + + return nil + }) + + return plugins +} diff --git a/pkg/types/plugins.go b/pkg/types/plugins.go index 7176e382..fa57b8f1 100644 --- a/pkg/types/plugins.go +++ b/pkg/types/plugins.go @@ -28,6 +28,13 @@ type PluginConfig interface { Set() } +type PluginMetadata struct { + Name string + IsDependable bool + IsExposable bool + DependsOn []string `json:",omitempty"` +} + type PluginManagers struct { SessionManager SessionManager WebSocketManager WebSocketManager