github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/cmd/gosbom/cli/eventloop/event_loop.go (about) 1 package eventloop 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 8 "github.com/hashicorp/go-multierror" 9 "github.com/nextlinux/gosbom/internal/log" 10 "github.com/nextlinux/gosbom/internal/ui" 11 "github.com/wagoodman/go-partybus" 12 ) 13 14 // eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and 15 // signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until 16 // an eventual graceful exit. 17 func EventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error { 18 defer cleanupFn() 19 events := subscription.Events() 20 var err error 21 var ux ui.UI 22 23 if ux, err = setupUI(subscription.Unsubscribe, uxs...); err != nil { 24 return err 25 } 26 27 var retErr error 28 var forceTeardown bool 29 30 for { 31 if workerErrs == nil && events == nil { 32 break 33 } 34 select { 35 case err, isOpen := <-workerErrs: 36 if !isOpen { 37 workerErrs = nil 38 continue 39 } 40 if err != nil { 41 // capture the error from the worker and unsubscribe to complete a graceful shutdown 42 retErr = multierror.Append(retErr, err) 43 _ = subscription.Unsubscribe() 44 // the worker has exited, we may have been mid-handling events for the UI which should now be 45 // ignored, in which case forcing a teardown of the UI irregardless of the state is required. 46 forceTeardown = true 47 } 48 case e, isOpen := <-events: 49 if !isOpen { 50 events = nil 51 continue 52 } 53 54 if err := ux.Handle(e); err != nil { 55 if errors.Is(err, partybus.ErrUnsubscribe) { 56 events = nil 57 } else { 58 retErr = multierror.Append(retErr, err) 59 // TODO: should we unsubscribe? should we try to halt execution? or continue? 60 } 61 } 62 case <-signals: 63 // ignore further results from any event source and exit ASAP, but ensure that all cache is cleaned up. 64 // we ignore further errors since cleaning up the tmp directories will affect running catalogers that are 65 // reading/writing from/to their nested temp dirs. This is acceptable since we are bailing without result. 66 67 // TODO: potential future improvement would be to pass context into workers with a cancel function that is 68 // to the event loop. In this way we can have a more controlled shutdown even at the most nested levels 69 // of processing. 70 events = nil 71 workerErrs = nil 72 forceTeardown = true 73 } 74 } 75 76 if err := ux.Teardown(forceTeardown); err != nil { 77 retErr = multierror.Append(retErr, err) 78 } 79 80 return retErr 81 } 82 83 // setupUI takes one or more UIs that responds to events and takes a event bus unsubscribe function for use 84 // during teardown. With the given UIs, the first UI which the ui.Setup() function does not return an error 85 // will be utilized in execution. Providing a set of UIs allows for the caller to provide graceful fallbacks 86 // when there are environmental problem (e.g. unable to setup a TUI with the current TTY). 87 func setupUI(unsubscribe func() error, uis ...ui.UI) (ui.UI, error) { 88 for _, ux := range uis { 89 if err := ux.Setup(unsubscribe); err != nil { 90 log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err) 91 continue 92 } 93 94 return ux, nil 95 } 96 return nil, fmt.Errorf("unable to setup any UI") 97 }