package modules import ( "context" "errors" "fmt" "github.com/safing/portbase/log" "github.com/tevino/abool" ) type eventHooks struct { // hooks holds all registed hooks for the event. hooks []*eventHook // internal signifies that the event and it's data may not be exposed and may // only be propagated internally. internal bool } type eventHookFn func(context.Context, interface{}) error type eventHook struct { description string hookingModule *Module hookFn eventHookFn } // TriggerEvent executes all hook functions registered to the specified event. func (m *Module) TriggerEvent(event string, data interface{}) { if m.OnlineSoon() { go m.processEventTrigger(event, data) } } func (m *Module) processEventTrigger(event string, data interface{}) { m.eventHooksLock.RLock() defer m.eventHooksLock.RUnlock() eventHooks, ok := m.eventHooks[event] if !ok { log.Warningf(`%s: tried to trigger non-existent event "%s"`, m.Name, event) return } for _, hook := range eventHooks.hooks { if hook.hookingModule.OnlineSoon() { go m.runEventHook(hook, event, data) } } // Call subscription function, if set. if eventSubscriptionFuncReady.IsSet() { m.StartWorker("event subscription", func(context.Context) error { // Only use data in worker that won't change anymore. eventSubscriptionFunc(m.Name, event, eventHooks.internal, data) return nil }) } } // InjectEvent triggers an event from a foreign module and executes all hook functions registered to that event. func (m *Module) InjectEvent(sourceEventName, targetModuleName, targetEventName string, data interface{}) error { if !m.OnlineSoon() { return errors.New("module not yet started") } if !modulesLocked.IsSet() { return errors.New("module system not yet started") } targetModule, ok := modules[targetModuleName] if !ok { return fmt.Errorf(`module "%s" does not exist`, targetModuleName) } targetModule.eventHooksLock.RLock() defer targetModule.eventHooksLock.RUnlock() targetHooks, ok := targetModule.eventHooks[targetEventName] if !ok { return fmt.Errorf(`module "%s" has no event named "%s"`, targetModuleName, targetEventName) } for _, hook := range targetHooks.hooks { if hook.hookingModule.OnlineSoon() { go m.runEventHook(hook, sourceEventName, data) } } // Call subscription function, if set. if eventSubscriptionFuncReady.IsSet() { m.StartWorker("event subscription", func(context.Context) error { // Only use data in worker that won't change anymore. eventSubscriptionFunc(targetModule.Name, targetEventName, targetHooks.internal, data) return nil }) } return nil } func (m *Module) runEventHook(hook *eventHook, event string, data interface{}) { // check if source module is ready for handling if m.Status() != StatusOnline { // target module has not yet fully started, wait until start is complete select { case <-m.StartCompleted(): // continue with hook execution case <-hook.hookingModule.Stopping(): return case <-m.Stopping(): return } } // check if destionation module is ready for handling if hook.hookingModule.Status() != StatusOnline { // target module has not yet fully started, wait until start is complete select { case <-hook.hookingModule.StartCompleted(): // continue with hook execution case <-hook.hookingModule.Stopping(): return case <-m.Stopping(): return } } err := hook.hookingModule.RunWorker( fmt.Sprintf("event hook %s/%s -> %s/%s", m.Name, event, hook.hookingModule.Name, hook.description), func(ctx context.Context) error { return hook.hookFn(ctx, data) }, ) if err != nil { log.Warningf("%s: failed to execute event hook %s/%s -> %s/%s: %s", hook.hookingModule.Name, m.Name, event, hook.hookingModule.Name, hook.description, err) } } // RegisterEvent registers a new event to allow for registering hooks. // The expose argument controls whether these events and the attached data may // be received by external components via APIs. If not exposed, the database // record that carries the event and it's data will be marked as secret and as // a crown jewel. Enforcement is left to the database layer. func (m *Module) RegisterEvent(event string, expose bool) { m.eventHooksLock.Lock() defer m.eventHooksLock.Unlock() _, ok := m.eventHooks[event] if !ok { m.eventHooks[event] = &eventHooks{ hooks: make([]*eventHook, 0, 1), internal: !expose, } } } // RegisterEventHook registers a hook function with (another) modules' event. Whenever a hook is triggered and the receiving module has not yet fully started, hook execution will be delayed until the modules completed starting. func (m *Module) RegisterEventHook(module string, event string, description string, fn func(context.Context, interface{}) error) error { // get target module var eventModule *Module if module == m.Name { eventModule = m } else { var ok bool eventModule, ok = modules[module] if !ok { return fmt.Errorf(`module "%s" does not exist`, module) } } // get target event eventModule.eventHooksLock.Lock() defer eventModule.eventHooksLock.Unlock() eventHooks, ok := eventModule.eventHooks[event] if !ok { return fmt.Errorf(`event "%s/%s" does not exist`, eventModule.Name, event) } // add hook eventHooks.hooks = append(eventHooks.hooks, &eventHook{ description: description, hookingModule: m, hookFn: fn, }) return nil } // Subscribe to all events var ( eventSubscriptionFunc func(moduleName, eventName string, internal bool, data interface{}) eventSubscriptionFuncEnabled = abool.NewBool(false) eventSubscriptionFuncReady = abool.NewBool(false) ) // SetEventSubscriptionFunc func SetEventSubscriptionFunc(fn func(moduleName, eventName string, internal bool, data interface{})) bool { if eventSubscriptionFuncEnabled.SetToIf(false, true) { eventSubscriptionFunc = fn eventSubscriptionFuncReady.Set() return true } return false }