diff --git a/cmd/event_loop.go b/cmd/event_loop.go new file mode 100644 index 000000000..4a86f1bae --- /dev/null +++ b/cmd/event_loop.go @@ -0,0 +1,77 @@ +package cmd + +import ( + "errors" + "os" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/internal/ui" + "github.com/hashicorp/go-multierror" + "github.com/wagoodman/go-partybus" +) + +// eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and +// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until +// an eventual graceful exit. +func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, ux ui.UI) error { + events := subscription.Events() + if err := setupUI(subscription.Unsubscribe, ux); err != nil { + return err + } + + var retErr error + + for { + if workerErrs == nil && events == nil { + break + } + select { + case err, isOpen := <-workerErrs: + if !isOpen { + workerErrs = nil + continue + } + if err != nil { + retErr = err + } + case e, isOpen := <-events: + if !isOpen { + events = nil + continue + } + + if err := ux.Handle(e); err != nil { + if errors.Is(err, partybus.ErrUnsubscribe) { + log.Warnf("unable to unsubscribe from the event bus") + events = nil + } else { + retErr = multierror.Append(retErr, err) + // TODO: should we unsubscribe? should we try to halt execution? or continue? + } + } + case <-signals: + if err := subscription.Unsubscribe(); err != nil { + log.Warnf("unable to unsubscribe from the event bus: %+v", err) + events = nil + } + } + } + + if err := ux.Teardown(); err != nil { + retErr = multierror.Append(retErr, err) + } + + return retErr +} + +func setupUI(unsubscribe func() error, ux ui.UI) error { + if err := ux.Setup(unsubscribe); err != nil { + ux = ui.NewLoggerUI() + if err := ux.Setup(unsubscribe); err != nil { + // something is very wrong, bail. + return err + } + log.Errorf("unable to setup given UI, falling back to logger: %+v", err) + } + return nil +} diff --git a/cmd/packages.go b/cmd/packages.go index b4882db0d..4fae5c41c 100644 --- a/cmd/packages.go +++ b/cmd/packages.go @@ -186,9 +186,12 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error { } func packagesExec(_ *cobra.Command, args []string) error { - errs := packagesExecWorker(args[0]) - ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet) - return ux(errs, eventSubscription) + return eventLoop( + packagesExecWorker(args[0]), + setupSignals(), + eventSubscription, + ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet), + ) } func packagesExecWorker(userInput string) <-chan error { diff --git a/cmd/power_user.go b/cmd/power_user.go index 2234d5675..a5728fa64 100644 --- a/cmd/power_user.go +++ b/cmd/power_user.go @@ -63,9 +63,12 @@ func init() { } func powerUserExec(_ *cobra.Command, args []string) error { - errs := powerUserExecWorker(args[0]) - ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet) - return ux(errs, eventSubscription) + return eventLoop( + powerUserExecWorker(args[0]), + setupSignals(), + eventSubscription, + ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet), + ) } func powerUserExecWorker(userInput string) <-chan error { diff --git a/cmd/signals.go b/cmd/signals.go new file mode 100644 index 000000000..f20379d1e --- /dev/null +++ b/cmd/signals.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "os" + "os/signal" + "syscall" +) + +func setupSignals() <-chan os.Signal { + c := make(chan os.Signal, 1) // Note: A buffered channel is recommended for this; see https://golang.org/pkg/os/signal/#Notify + + interruptions := []os.Signal{ + syscall.SIGINT, + syscall.SIGTERM, + } + + signal.Notify(c, interruptions...) + + return c +} diff --git a/go.mod b/go.mod index 0d309c351..89e6352f8 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/gookit/color v1.2.7 github.com/hashicorp/go-multierror v1.1.0 github.com/hashicorp/go-version v1.2.0 + github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.3.1 github.com/olekukonko/tablewriter v0.0.4 @@ -36,7 +37,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.0 github.com/stretchr/testify v1.6.0 - github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d + github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240 github.com/wagoodman/jotframe v0.0.0-20200730190914-3517092dd163 github.com/x-cray/logrus-prefixed-formatter v0.5.2 diff --git a/go.sum b/go.sum index d58de9377..71d5f646a 100644 --- a/go.sum +++ b/go.sum @@ -466,6 +466,7 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -744,8 +745,9 @@ github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOV github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/vdemeester/k8s-pkg-credentialprovider v1.17.4/go.mod h1:inCTmtUdr5KJbreVojo06krnTgaeAz/Z7lynpPk/Q2c= github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= -github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d h1:KOxOL6qpmqwoPloNwi+CEgc1ayjHNOFNrvoOmeDOjDg= github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d/go.mod h1:JPirS5jde/CF5qIjcK4WX+eQmKXdPc6vcZkJ/P0hfPw= +github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 h1:phTLPgMRDYTizrBSKsNSOa2zthoC2KsJsaY/8sg3rD8= +github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5/go.mod h1:JPirS5jde/CF5qIjcK4WX+eQmKXdPc6vcZkJ/P0hfPw= github.com/wagoodman/go-progress v0.0.0-20200621122631-1a2120f0695a/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240 h1:r6BlIP7CVZtMlxUQhT40h1IE1TzEgKVqwmsVGuscvdk= github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= diff --git a/internal/ui/common/event_handlers.go b/internal/ui/common/event_handlers.go index b601e6ac7..3d039deee 100644 --- a/internal/ui/common/event_handlers.go +++ b/internal/ui/common/event_handlers.go @@ -1,8 +1,15 @@ package common import ( + "context" "fmt" + "io" "os" + "sync" + + "github.com/anchore/syft/internal" + "github.com/gookit/color" + "github.com/wagoodman/jotframe/pkg/frame" syftEventParsers "github.com/anchore/syft/syft/event/parsers" "github.com/wagoodman/go-partybus" @@ -22,3 +29,21 @@ func CatalogerPresenterReady(event partybus.Event) error { } return nil } + +// appUpdateAvailableHandler is a UI handler function to display a new application version to the top of the screen. +func AppUpdateAvailableHandler(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error { + newVersion, err := syftEventParsers.ParseAppUpdateAvailable(event) + if err != nil { + return fmt.Errorf("bad AppUpdateAvailable event: %w", err) + } + + line, err := fr.Prepend() + if err != nil { + return err + } + + message := color.Magenta.Sprintf("New version of %s is available: %s", internal.ApplicationName, newVersion) + _, _ = io.WriteString(line, message) + + return nil +} diff --git a/internal/ui/common/spinner.go b/internal/ui/common/spinner.go index fa86c670e..abf48f32b 100644 --- a/internal/ui/common/spinner.go +++ b/internal/ui/common/spinner.go @@ -8,33 +8,9 @@ import ( // TODO: move me to a common module (used in multiple repos) const ( - SpinnerCircleOutlineSet = "◜◠◯◎◉●◉◎◯◡◞" - SpinnerCircleSet = "◌◯◎◉●◉◎◯" - SpinnerDotSet = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" - SpinnerHorizontalBarSet = "▉▊▋▌▍▎▏▎▍▌▋▊▉" - SpinnerVerticalBarSet = "▁▃▄▅▆▇█▇▆▅▄▃▁" - SpinnerDoubleBarSet = "▁▂▃▄▅▆▇█▉▊▋▌▍▎▏▏▎▍▌▋▊▉█▇▆▅▄▃▂▁" - SpinnerArrowSet = "←↖↑↗→↘↓↙" + SpinnerDotSet = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" ) -var SpinnerCircleDotSet = []string{ - "⠈⠁", - "⠈⠑", - "⠈⠱", - "⠈⡱", - "⢀⡱", - "⢄⡱", - "⢄⡱", - "⢆⡱", - "⢎⡱", - "⢎⡰", - "⢎⡠", - "⢎⡀", - "⢎⠁", - "⠎⠁", - "⠊⠁", -} - type Spinner struct { index int charset []string @@ -47,12 +23,6 @@ func NewSpinner(charset string) Spinner { } } -func NewSpinnerFromSlice(charset []string) Spinner { - return Spinner{ - charset: charset, - } -} - func (s *Spinner) Current() string { s.lock.Lock() defer s.lock.Unlock() diff --git a/internal/ui/ephemeral_terminal_ui.go b/internal/ui/ephemeral_terminal_ui.go new file mode 100644 index 000000000..a970e8513 --- /dev/null +++ b/internal/ui/ephemeral_terminal_ui.go @@ -0,0 +1,138 @@ +package ui + +import ( + "bytes" + "context" + "fmt" + "os" + "sync" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/internal/logger" + "github.com/anchore/syft/internal/ui/common" + syftEvent "github.com/anchore/syft/syft/event" + "github.com/anchore/syft/ui" + "github.com/k0kubun/go-ansi" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/jotframe/pkg/frame" +) + +// ephemeralTerminalUI provides an "ephemeral" terminal user interface to display the application state dynamically. +// The terminal is placed into raw mode and the cursor is manipulated to allow for a dynamic, multi-line +// UI (provided by the jotframe lib), for this reason all other application mechanisms that write to the screen +// must be suppressed before starting (such as logs); since bytes in the device and in application memory combine to make +// a shared state, bytes coming from elsewhere to the screen will disrupt this state. +// +// This UI is primarily driven off of events from the event bus, creating single-line terminal widgets to represent a +// published element on the event bus, typically polling the element for the latest state. This allows for the UI to +// control update frequency to the screen, provide "liveness" indications that are interpolated between bus events, +// and overall loosely couple the bus events from screen interactions. +// +// By convention, all elements published on the bus should be treated as read-only, and publishers on the bus should +// attempt to enforce this when possible by wrapping complex objects with interfaces to prescribe interactions. Also by +// convention, each new event that the UI should respond to should be added either in this package as a handler function, +// or in the shared ui package as a function on the main handler object. All handler functions should be completed +// processing an event before the ETUI exits (coordinated with a sync.WaitGroup) +type ephemeralTerminalUI struct { + unsubscribe func() error + handler *ui.Handler + waitGroup *sync.WaitGroup + frame *frame.Frame + logBuffer *bytes.Buffer +} + +func NewEphemeralTerminalUI() UI { + return &ephemeralTerminalUI{ + handler: ui.NewHandler(), + waitGroup: &sync.WaitGroup{}, + } +} + +func (h *ephemeralTerminalUI) Setup(unsubscribe func() error) error { + h.unsubscribe = unsubscribe + ansi.CursorHide() + + // prep the logger to not clobber the screen from now on (logrus only) + h.logBuffer = bytes.NewBufferString("") + logWrapper, ok := log.Log.(*logger.LogrusLogger) + if ok { + logWrapper.Logger.SetOutput(h.logBuffer) + } + + return h.openScreen() +} + +func (h *ephemeralTerminalUI) Handle(event partybus.Event) error { + ctx := context.Background() + switch { + case h.handler.RespondsTo(event): + if err := h.handler.Handle(ctx, h.frame, event, h.waitGroup); err != nil { + log.Errorf("unable to show %s event: %+v", event.Type, err) + } + + case event.Type == syftEvent.AppUpdateAvailable: + if err := common.AppUpdateAvailableHandler(ctx, h.frame, event, h.waitGroup); err != nil { + log.Errorf("unable to show %s event: %+v", event.Type, err) + } + + case event.Type == syftEvent.PresenterReady: + // we need to close the screen now since signaling the the presenter is ready means that we + // are about to write bytes to stdout, so we should reset the terminal state first + h.closeScreen() + + if err := common.CatalogerPresenterReady(event); err != nil { + log.Errorf("unable to show %s event: %+v", event.Type, err) + } + + // this is the last expected event, stop listening to events + return h.unsubscribe() + } + return nil +} + +func (h *ephemeralTerminalUI) openScreen() error { + config := frame.Config{ + PositionPolicy: frame.PolicyFloatForward, + // only report output to stderr, reserve report output for stdout + Output: os.Stderr, + } + + fr, err := frame.New(config) + if err != nil { + return fmt.Errorf("failed to create the screen object: %w", err) + } + h.frame = fr + + return nil +} + +func (h *ephemeralTerminalUI) closeScreen() { + // we may have other background processes still displaying progress, wait for them to + // finish before discontinuing dynamic content and showing the final report + if !h.frame.IsClosed() { + h.waitGroup.Wait() + h.frame.Close() + // TODO: there is a race condition within frame.Close() that sometimes leads to an extra blank line being output + frame.Close() + + // only flush the log on close + h.flushLog() + } +} + +func (h *ephemeralTerminalUI) flushLog() { + // flush any errors to the screen before the report + logWrapper, ok := log.Log.(*logger.LogrusLogger) + if ok { + fmt.Fprint(logWrapper.Output, h.logBuffer.String()) + logWrapper.Logger.SetOutput(os.Stderr) + } else { + fmt.Fprint(os.Stderr, h.logBuffer.String()) + } +} + +func (h *ephemeralTerminalUI) Teardown() error { + h.closeScreen() + ansi.CursorShow() + return nil +} diff --git a/internal/ui/etui/ephemeral_tui.go b/internal/ui/etui/ephemeral_tui.go deleted file mode 100644 index fa5a0a61b..000000000 --- a/internal/ui/etui/ephemeral_tui.go +++ /dev/null @@ -1,180 +0,0 @@ -/* -Package etui provides an "ephemeral" terminal user interface to display the application state dynamically. -The terminal is placed into raw mode and the cursor is manipulated to allow for a dynamic, multi-line -UI (provided by the jotframe lib), for this reason all other application mechanisms that write to the screen -must be suppressed before starting (such as logs); since bytes in the device and in application memory combine to make -a shared state, bytes coming from elsewhere to the screen will disrupt this state. - -This UI is primarily driven off of events from the event bus, creating single-line terminal widgets to represent a -published element on the event bus, typically polling the element for the latest state. This allows for the UI to -control update frequency to the screen, provide "liveness" indications that are interpolated between bus events, -and overall loosely couple the bus events from screen interactions. - -By convention, all elements published on the bus should be treated as read-only, and publishers on the bus should -attempt to enforce this when possible by wrapping complex objects with interfaces to prescribe interactions. Also by -convention, each new event that the UI should respond to should be added either in this package as a handler function, -or in the shared ui package as a function on the main handler object. All handler functions should be completed -processing an event before the ETUI exits (coordinated with a sync.WaitGroup) -*/ -package etui - -import ( - "bytes" - "context" - "fmt" - "os" - "os/signal" - "sync" - "syscall" - - "github.com/anchore/syft/internal/logger" - - "github.com/anchore/syft/internal/ui/common" - "github.com/anchore/syft/ui" - - "github.com/anchore/syft/internal/log" - syftEvent "github.com/anchore/syft/syft/event" - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/jotframe/pkg/frame" -) - -// TODO: specify per-platform implementations with build tags (needed when windows is supported by syft) - -// setupScreen creates a new jotframe object to manage specific screen lines dynamically, preparing the screen device -// as needed (i.e. setting the terminal to raw mode). -func setupScreen(output *os.File) *frame.Frame { - config := frame.Config{ - PositionPolicy: frame.PolicyFloatForward, - // only report output to stderr, reserve report output for stdout - Output: output, - } - - fr, err := frame.New(config) - if err != nil { - log.Errorf("failed to create screen object: %+v", err) - return nil - } - return fr -} - -// nolint:funlen,gocognit -// OutputToEphemeralTUI is a UI function that provides a terminal UI experience without a separate, in-application -// screen buffer. All logging is suppressed, buffered to a string, and output after the ETUI has been torn down. -func OutputToEphemeralTUI(workerErrs <-chan error, subscription *partybus.Subscription) error { - output := os.Stderr - - // prep the logger to not clobber the screen from now on (logrus only) - logBuffer := bytes.NewBufferString("") - logWrapper, ok := log.Log.(*logger.LogrusLogger) - if ok { - logWrapper.Logger.SetOutput(logBuffer) - } - - // hide cursor - _, _ = fmt.Fprint(output, "\x1b[?25l") - // show cursor - defer fmt.Fprint(output, "\x1b[?25h") - - fr := setupScreen(output) - if fr == nil { - return fmt.Errorf("unable to setup screen") - } - var isClosed bool - defer func() { - if !isClosed { - fr.Close() - frame.Close() - // flush any errors to the screen before the report - logWrapper, ok := log.Log.(*logger.LogrusLogger) - if ok { - fmt.Fprint(logWrapper.Output, logBuffer.String()) - } else { - fmt.Fprint(output, logBuffer.String()) - } - } - logWrapper, ok := log.Log.(*logger.LogrusLogger) - if ok { - logWrapper.Logger.SetOutput(output) - } - }() - - var err error - var wg = &sync.WaitGroup{} - events := subscription.Events() - ctx := context.Background() - syftUIHandler := ui.NewHandler() - - signals := interruptingSignals() - -eventLoop: - for { - select { - case err := <-workerErrs: - // TODO: we should show errors more explicitly in the ETUI - if err != nil { - return err - } - case e, ok := <-events: - if !ok { - break eventLoop - } - switch { - case syftUIHandler.RespondsTo(e): - if err = syftUIHandler.Handle(ctx, fr, e, wg); err != nil { - log.Errorf("unable to show %s event: %+v", e.Type, err) - } - - case e.Type == syftEvent.AppUpdateAvailable: - if err = appUpdateAvailableHandler(ctx, fr, e, wg); err != nil { - log.Errorf("unable to show %s event: %+v", e.Type, err) - } - - case e.Type == syftEvent.PresenterReady: - // we may have other background processes still displaying progress, wait for them to - // finish before discontinuing dynamic content and showing the final report - wg.Wait() - fr.Close() - // TODO: there is a race condition within frame.Close() that sometimes leads to an extra blank line being output - frame.Close() - isClosed = true - - // flush any errors to the screen before the report - logWrapper, ok := log.Log.(*logger.LogrusLogger) - if ok { - fmt.Fprint(logWrapper.Output, logBuffer.String()) - } else { - fmt.Fprint(output, logBuffer.String()) - } - - if err := common.CatalogerPresenterReady(e); err != nil { - log.Errorf("unable to show %s event: %+v", e.Type, err) - } - - // this is the last expected event - break eventLoop - } - case <-ctx.Done(): - if ctx.Err() != nil { - log.Errorf("cancelled (%+v)", err) - } - break eventLoop - case <-signals: - break eventLoop - } - } - - return nil -} - -func interruptingSignals() chan os.Signal { - c := make(chan os.Signal, 1) // Note: A buffered channel is recommended for this; see https://golang.org/pkg/os/signal/#Notify - - interruptions := []os.Signal{ - syscall.SIGINT, - syscall.SIGTERM, - } - - signal.Notify(c, interruptions...) - - return c -} diff --git a/internal/ui/etui/internal_event_handlers.go b/internal/ui/etui/internal_event_handlers.go deleted file mode 100644 index 3df361a66..000000000 --- a/internal/ui/etui/internal_event_handlers.go +++ /dev/null @@ -1,32 +0,0 @@ -package etui - -import ( - "context" - "fmt" - "io" - "sync" - - "github.com/anchore/syft/internal" - syftEventParsers "github.com/anchore/syft/syft/event/parsers" - "github.com/gookit/color" - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/jotframe/pkg/frame" -) - -// appUpdateAvailableHandler is a UI handler function to display a new application version to the top of the screen. -func appUpdateAvailableHandler(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error { - newVersion, err := syftEventParsers.ParseAppUpdateAvailable(event) - if err != nil { - return fmt.Errorf("bad AppUpdateAvailable event: %w", err) - } - - line, err := fr.Prepend() - if err != nil { - return err - } - - message := color.Magenta.Sprintf("New version of %s is available: %s", internal.ApplicationName, newVersion) - _, _ = io.WriteString(line, message) - - return nil -} diff --git a/internal/ui/logger_output.go b/internal/ui/logger_output.go deleted file mode 100644 index d402958fe..000000000 --- a/internal/ui/logger_output.go +++ /dev/null @@ -1,40 +0,0 @@ -package ui - -import ( - "github.com/anchore/syft/internal/log" - "github.com/anchore/syft/internal/ui/common" - syftEvent "github.com/anchore/syft/syft/event" - "github.com/wagoodman/go-partybus" -) - -// LoggerUI is a UI function that leverages the displays all application logs to the screen. -func LoggerUI(workerErrs <-chan error, subscription *partybus.Subscription) error { - events := subscription.Events() -eventLoop: - for { - select { - case err := <-workerErrs: - if err != nil { - return err - } - case e, ok := <-events: - if !ok { - // event bus closed... - break eventLoop - } - - // ignore all events except for the final event - if e.Type == syftEvent.PresenterReady { - err := common.CatalogerPresenterReady(e) - if err != nil { - log.Errorf("unable to show catalog image finished event: %+v", err) - } - - // this is the last expected event - break eventLoop - } - } - } - - return nil -} diff --git a/internal/ui/logger_ui.go b/internal/ui/logger_ui.go new file mode 100644 index 000000000..178713364 --- /dev/null +++ b/internal/ui/logger_ui.go @@ -0,0 +1,40 @@ +package ui + +import ( + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/internal/ui/common" + syftEvent "github.com/anchore/syft/syft/event" + "github.com/wagoodman/go-partybus" +) + +type loggerUI struct { + unsubscribe func() error +} + +func NewLoggerUI() UI { + return &loggerUI{} +} + +func (l *loggerUI) Setup(unsubscribe func() error) error { + l.unsubscribe = unsubscribe + return nil +} + +func (l loggerUI) Handle(event partybus.Event) error { + // ignore all events except for the final event + if event.Type != syftEvent.PresenterReady { + return nil + } + + err := common.CatalogerPresenterReady(event) + if err != nil { + log.Errorf("unable to show catalog image finished event: %+v", err) + } + + // this is the last expected event, stop listening to events + return l.unsubscribe() +} + +func (l loggerUI) Teardown() error { + return nil +} diff --git a/internal/ui/select.go b/internal/ui/select.go index b0da99633..37969efb1 100644 --- a/internal/ui/select.go +++ b/internal/ui/select.go @@ -4,7 +4,6 @@ import ( "os" "runtime" - "github.com/anchore/syft/internal/ui/etui" "golang.org/x/crypto/ssh/terminal" ) @@ -21,9 +20,9 @@ func Select(verbose, quiet bool) UI { switch { case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty: - ui = LoggerUI + ui = NewLoggerUI() default: - ui = etui.OutputToEphemeralTUI + ui = NewEphemeralTerminalUI() } return ui diff --git a/internal/ui/ui.go b/internal/ui/ui.go index c9b77af3e..5e41b400f 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -4,6 +4,8 @@ import ( "github.com/wagoodman/go-partybus" ) -// UI is a function that takes a channel of errors from the main() worker and a event bus subscription and is -// responsible for displaying pertinent events to the user, on the screen or otherwise. -type UI func(<-chan error, *partybus.Subscription) error +type UI interface { + Setup(unsubscribe func() error) error + partybus.Handler + Teardown() error +} diff --git a/syft/source/source.go b/syft/source/source.go index 1d79dea4b..71cb77fd3 100644 --- a/syft/source/source.go +++ b/syft/source/source.go @@ -49,9 +49,7 @@ func New(userInput string, registryOptions *image.RegistryOptions) (Source, func case ImageScheme: img, err := stereoscope.GetImageFromSource(location, imageSource, registryOptions) - cleanup := func() { - stereoscope.Cleanup() - } + cleanup := stereoscope.Cleanup if err != nil || img == nil { return Source{}, cleanup, fmt.Errorf("could not fetch image '%s': %w", location, err) diff --git a/ui/event_handlers.go b/ui/event_handlers.go index b7a6363e9..8b2b4ceaa 100644 --- a/ui/event_handlers.go +++ b/ui/event_handlers.go @@ -8,11 +8,12 @@ import ( "sync" "time" + "github.com/anchore/syft/internal/ui/common" + "github.com/anchore/stereoscope/pkg/image/docker" "github.com/dustin/go-humanize" stereoEventParsers "github.com/anchore/stereoscope/pkg/event/parsers" - "github.com/anchore/syft/internal/ui/common" syftEventParsers "github.com/anchore/syft/syft/event/parsers" "github.com/gookit/color" "github.com/wagoodman/go-partybus"