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  }