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