package party import ( "fmt" "strings" "sync" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/phrasebook" "git.golem15.com/golem15/summercms/modules/postcard" ) // Plugin is the required descriptor every compiled plugin implements. type Plugin interface { ID() string Requires() []string Register(app *backpack.App) error Boot(app *backpack.App) error } var ( mu sync.Mutex plugins []Plugin ) // Register appends a plugin to the process-wide registry. Plugins call this // from init() after a blank import; Activate selects and orders them. func Register(p Plugin) { if p == nil { return } mu.Lock() defer mu.Unlock() plugins = append(plugins, p) } func snapshot() []Plugin { mu.Lock() defer mu.Unlock() out := make([]Plugin, len(plugins)) copy(out, plugins) return out } // Activate resolves ids in manifest order, topologically sorts Requires(), // then runs every Register before any Boot. func Activate(app *backpack.App, ids []string) ([]Plugin, error) { if app == nil { return nil, fmt.Errorf("party: app is nil") } return activate(snapshot(), app, ids) } func activate(registered []Plugin, app *backpack.App, ids []string) ([]Plugin, error) { byID := make(map[string]Plugin, len(registered)) for _, p := range registered { if p == nil { continue } id := p.ID() if id == "" { return nil, fmt.Errorf("party: registered plugin has empty id") } if _, exists := byID[id]; exists { return nil, fmt.Errorf("party: duplicate plugin id %q", id) } byID[id] = p } seen := make(map[string]struct{}, len(ids)) selected := make([]Plugin, 0, len(ids)) index := make(map[string]int, len(ids)) for _, id := range ids { if id == "" { return nil, fmt.Errorf("party: empty plugin id in manifest") } if _, dup := seen[id]; dup { return nil, fmt.Errorf("party: duplicate plugin id %q", id) } seen[id] = struct{}{} p, ok := byID[id] if !ok { return nil, fmt.Errorf("party: plugin %q is not registered", id) } index[id] = len(selected) selected = append(selected, p) } for _, p := range selected { for _, req := range p.Requires() { if _, ok := seen[req]; !ok { return nil, fmt.Errorf("party: plugin %q requires %q, which is not registered", p.ID(), req) } } } ordered, err := topoSort(selected, index) if err != nil { return nil, err } orderedIDs := make([]string, len(ordered)) for i, p := range ordered { orderedIDs[i] = p.ID() } app.SetPlugins(orderedIDs) for _, p := range ordered { if hc, ok := p.(pact.HasConfig); ok && app.Config != nil { if err := app.Config.MergePlugin(p.ID(), hc.ConfigFS()); err != nil { return nil, fmt.Errorf("party: config %s: %w", p.ID(), err) } } } for _, p := range ordered { if err := p.Register(app); err != nil { return nil, fmt.Errorf("party: register %s: %w", p.ID(), err) } } if err := phrasebook.Activate(app, ordered); err != nil { return nil, fmt.Errorf("party: lang: %w", err) } if err := postcard.Activate(app, ordered); err != nil { return nil, fmt.Errorf("party: mail: %w", err) } for _, p := range ordered { if err := postcard.BootPlugin(app, p); err != nil { return nil, fmt.Errorf("party: boot %s: %w", p.ID(), err) } if err := p.Boot(app); err != nil { return nil, fmt.Errorf("party: boot %s: %w", p.ID(), err) } } return ordered, nil } func topoSort(plugins []Plugin, index map[string]int) ([]Plugin, error) { byID := make(map[string]Plugin, len(plugins)) indeg := make(map[string]int, len(plugins)) dependents := make(map[string][]string, len(plugins)) for _, p := range plugins { byID[p.ID()] = p indeg[p.ID()] = 0 } for _, p := range plugins { seenReq := make(map[string]struct{}) for _, req := range p.Requires() { if _, ok := seenReq[req]; ok { continue } seenReq[req] = struct{}{} if _, ok := byID[req]; !ok { continue } indeg[p.ID()]++ dependents[req] = append(dependents[req], p.ID()) } } ready := make([]string, 0, len(plugins)) for _, p := range plugins { if indeg[p.ID()] == 0 { ready = append(ready, p.ID()) } } out := make([]Plugin, 0, len(plugins)) for len(ready) > 0 { id := ready[0] ready = ready[1:] out = append(out, byID[id]) next := append([]string(nil), dependents[id]...) sortByIndex(next, index) for _, dep := range next { indeg[dep]-- if indeg[dep] == 0 { ready = insertByIndex(ready, dep, index) } } } if len(out) != len(plugins) { leftover := make([]string, 0) done := make(map[string]struct{}, len(out)) for _, p := range out { done[p.ID()] = struct{}{} } for _, p := range plugins { if _, ok := done[p.ID()]; !ok { leftover = append(leftover, p.ID()) } } return nil, fmt.Errorf("party: dependency cycle involving %s", strings.Join(leftover, ", ")) } return out, nil } func sortByIndex(ids []string, index map[string]int) { for i := 1; i < len(ids); i++ { j := i for j > 0 && index[ids[j]] < index[ids[j-1]] { ids[j], ids[j-1] = ids[j-1], ids[j] j-- } } } func insertByIndex(ready []string, id string, index map[string]int) []string { at := len(ready) for i, existing := range ready { if index[id] < index[existing] { at = i break } } ready = append(ready, "") copy(ready[at+1:], ready[at:]) ready[at] = id return ready }