From a5e72815631610d0232305c5ca3ab114d2ce6f99 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 5 Oct 2021 13:07:20 -0400 Subject: [PATCH 1/6] add output to file option Signed-off-by: Alex Goodman --- cmd/event_loop.go | 2 +- cmd/packages.go | 19 ++++++++++- cmd/power_user.go | 10 +++++- cmd/report_writer.go | 29 ++++++++++++++++ cmd/root.go | 11 ++++++ internal/config/application.go | 1 + .../snapshot/TestJSONPresenter.golden | 1 + internal/ui/ephemeral_terminal_ui.go | 34 ++++++++++--------- internal/ui/event_handlers.go | 5 ++- internal/ui/logger_ui.go | 13 ++++--- internal/ui/select.go | 7 ++-- 11 files changed, 103 insertions(+), 29 deletions(-) create mode 100644 cmd/report_writer.go diff --git a/cmd/event_loop.go b/cmd/event_loop.go index 72cddb9718e..77f340776b5 100644 --- a/cmd/event_loop.go +++ b/cmd/event_loop.go @@ -81,7 +81,7 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription * func setupUI(unsubscribe func() error, ux ui.UI) (ui.UI, error) { if err := ux.Setup(unsubscribe); err != nil { // replace the existing UI with a (simpler) logger UI - ux = ui.NewLoggerUI() + ux = ui.NewLoggerUI(os.Stdout) if err := ux.Setup(unsubscribe); err != nil { // something is very wrong, bail. return ux, err diff --git a/cmd/packages.go b/cmd/packages.go index 931640ba06d..b98062c83f2 100644 --- a/cmd/packages.go +++ b/cmd/packages.go @@ -113,6 +113,11 @@ func setPackageFlags(flags *pflag.FlagSet) { fmt.Sprintf("report output formatter, options=%v", packages.AllPresenters), ) + flags.StringP( + "file", "", "", + "file to write the report output to (default is STDOUT)", + ) + ///////// Upload options ////////////////////////////////////////////////////////// flags.StringP( "host", "H", "", @@ -156,6 +161,10 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error { return err } + if err := viper.BindPFlag("file", flags.Lookup("file")); err != nil { + return err + } + ///////// Upload options ////////////////////////////////////////////////////////// if err := viper.BindPFlag("anchore.host", flags.Lookup("host")); err != nil { @@ -188,11 +197,19 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error { func packagesExec(_ *cobra.Command, args []string) error { // could be an image or a directory, with or without a scheme userInput := args[0] + + reporter, closer, err := reportWriter() + defer closer() + + if err != nil { + return err + } + return eventLoop( packagesExecWorker(userInput), setupSignals(), eventSubscription, - ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet), + ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter), stereoscope.Cleanup, ) } diff --git a/cmd/power_user.go b/cmd/power_user.go index 4121322024d..bfef9a8ceab 100644 --- a/cmd/power_user.go +++ b/cmd/power_user.go @@ -73,11 +73,19 @@ func init() { func powerUserExec(_ *cobra.Command, args []string) error { // could be an image or a directory, with or without a scheme userInput := args[0] + + reporter, closer, err := reportWriter() + defer closer() + + if err != nil { + return err + } + return eventLoop( powerUserExecWorker(userInput), setupSignals(), eventSubscription, - ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet), + ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter), stereoscope.Cleanup, ) } diff --git a/cmd/report_writer.go b/cmd/report_writer.go new file mode 100644 index 00000000000..cb433b9c37c --- /dev/null +++ b/cmd/report_writer.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "fmt" + "io" + "os" + "strings" +) + +func reportWriter() (io.Writer, func() error, error) { + nop := func() error { return nil } + + path := strings.TrimSpace(appConfig.File) + switch len(path) { + case 0: + return os.Stdout, nop, nil + default: + reportFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return nil, nop, fmt.Errorf("unable to create report file: %w", err) + } + return reportFile, func() error { + if !appConfig.Quiet { + _, _ = fmt.Fprintf(os.Stderr, "Report written to %q\n", path) + } + return reportFile.Close() + }, nil + } +} diff --git a/cmd/root.go b/cmd/root.go index cb131972e61..d33b47f2a39 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -40,6 +40,17 @@ func init() { rootCmd.PersistentFlags().CountVarP(&persistentOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)") + rootCmd.PersistentFlags().StringP( + "file", "f", "", + "file to write the report output to (default is STDOUT)", + ) + + flag = "file" + if err := viper.BindPFlag(flag, rootCmd.PersistentFlags().Lookup(flag)); err != nil { + fmt.Printf("unable to bind flag '%s': %+v", flag, err) + os.Exit(1) + } + // set common options that are not universal (package subcommand-alias specific) setPackageFlags(rootCmd.Flags()) } diff --git a/internal/config/application.go b/internal/config/application.go index c8f7a1c4846..38a86a1cf42 100644 --- a/internal/config/application.go +++ b/internal/config/application.go @@ -29,6 +29,7 @@ type parser interface { type Application struct { ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading) Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting + File string `yaml:"file" json:"file" mapstructure:"file"` // -f, the file to write report output to Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI) CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not Anchore anchore `yaml:"anchore" json:"anchore" mapstructure:"anchore"` // options for interacting with Anchore Engine/Enterprise diff --git a/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden b/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden index d3ebc02531c..2f2002886bd 100644 --- a/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden +++ b/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden @@ -168,6 +168,7 @@ "configuration": { "configPath": "", "output": "", + "file": "", "quiet": false, "check-for-app-update": false, "anchore": { diff --git a/internal/ui/ephemeral_terminal_ui.go b/internal/ui/ephemeral_terminal_ui.go index 1d0d8028486..ab3063dc7b4 100644 --- a/internal/ui/ephemeral_terminal_ui.go +++ b/internal/ui/ephemeral_terminal_ui.go @@ -33,25 +33,27 @@ import ( // 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 - output *os.File + unsubscribe func() error + handler *ui.Handler + waitGroup *sync.WaitGroup + frame *frame.Frame + logBuffer *bytes.Buffer + uiOutput *os.File + reportOutput io.Writer } -func NewEphemeralTerminalUI() UI { +func NewEphemeralTerminalUI(reportWriter io.Writer) UI { return &ephemeralTerminalUI{ - handler: ui.NewHandler(), - waitGroup: &sync.WaitGroup{}, - output: os.Stderr, + handler: ui.NewHandler(), + waitGroup: &sync.WaitGroup{}, + uiOutput: os.Stderr, + reportOutput: reportWriter, } } func (h *ephemeralTerminalUI) Setup(unsubscribe func() error) error { h.unsubscribe = unsubscribe - hideCursor(h.output) + hideCursor(h.uiOutput) // prep the logger to not clobber the screen from now on (logrus only) h.logBuffer = bytes.NewBufferString("") @@ -81,7 +83,7 @@ func (h *ephemeralTerminalUI) Handle(event partybus.Event) error { // are about to write bytes to stdout, so we should reset the terminal state first h.closeScreen(false) - if err := handleCatalogerPresenterReady(event); err != nil { + if err := handleCatalogerPresenterReady(event, h.reportOutput); err != nil { log.Errorf("unable to show %s event: %+v", event.Type, err) } @@ -95,7 +97,7 @@ func (h *ephemeralTerminalUI) openScreen() error { config := frame.Config{ PositionPolicy: frame.PolicyFloatForward, // only report output to stderr, reserve report output for stdout - Output: h.output, + Output: h.uiOutput, } fr, err := frame.New(config) @@ -128,15 +130,15 @@ func (h *ephemeralTerminalUI) flushLog() { logWrapper, ok := log.Log.(*logger.LogrusLogger) if ok { fmt.Fprint(logWrapper.Output, h.logBuffer.String()) - logWrapper.Logger.SetOutput(h.output) + logWrapper.Logger.SetOutput(h.uiOutput) } else { - fmt.Fprint(h.output, h.logBuffer.String()) + fmt.Fprint(h.uiOutput, h.logBuffer.String()) } } func (h *ephemeralTerminalUI) Teardown(force bool) error { h.closeScreen(force) - showCursor(h.output) + showCursor(h.uiOutput) return nil } diff --git a/internal/ui/event_handlers.go b/internal/ui/event_handlers.go index 252f2b919ce..8daf706880c 100644 --- a/internal/ui/event_handlers.go +++ b/internal/ui/event_handlers.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "os" "sync" "github.com/anchore/syft/internal" @@ -17,14 +16,14 @@ import ( // handleCatalogerPresenterReady is a UI function for processing the CatalogerFinished bus event, displaying the catalog // via the given presenter to stdout. -func handleCatalogerPresenterReady(event partybus.Event) error { +func handleCatalogerPresenterReady(event partybus.Event, reportOutput io.Writer) error { // show the report to stdout pres, err := syftEventParsers.ParsePresenterReady(event) if err != nil { return fmt.Errorf("bad CatalogerFinished event: %w", err) } - if err := pres.Present(os.Stdout); err != nil { + if err := pres.Present(reportOutput); err != nil { return fmt.Errorf("unable to show package catalog report: %w", err) } return nil diff --git a/internal/ui/logger_ui.go b/internal/ui/logger_ui.go index 7108711b9bd..182623699b0 100644 --- a/internal/ui/logger_ui.go +++ b/internal/ui/logger_ui.go @@ -1,17 +1,22 @@ package ui import ( + "io" + "github.com/anchore/syft/internal/log" syftEvent "github.com/anchore/syft/syft/event" "github.com/wagoodman/go-partybus" ) type loggerUI struct { - unsubscribe func() error + unsubscribe func() error + reportOutput io.Writer } -func NewLoggerUI() UI { - return &loggerUI{} +func NewLoggerUI(reportWriter io.Writer) UI { + return &loggerUI{ + reportOutput: reportWriter, + } } func (l *loggerUI) Setup(unsubscribe func() error) error { @@ -25,7 +30,7 @@ func (l loggerUI) Handle(event partybus.Event) error { return nil } - if err := handleCatalogerPresenterReady(event); err != nil { + if err := handleCatalogerPresenterReady(event, l.reportOutput); err != nil { log.Warnf("unable to show catalog image finished event: %+v", err) } diff --git a/internal/ui/select.go b/internal/ui/select.go index 37969efb162..f3542fc9504 100644 --- a/internal/ui/select.go +++ b/internal/ui/select.go @@ -1,6 +1,7 @@ package ui import ( + "io" "os" "runtime" @@ -11,7 +12,7 @@ import ( // Select is responsible for determining the specific UI function given select user option, the current platform // config values, and environment status (such as a TTY being present). -func Select(verbose, quiet bool) UI { +func Select(verbose, quiet bool, reportWriter io.Writer) UI { var ui UI isStdoutATty := terminal.IsTerminal(int(os.Stdout.Fd())) @@ -20,9 +21,9 @@ func Select(verbose, quiet bool) UI { switch { case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty: - ui = NewLoggerUI() + ui = NewLoggerUI(reportWriter) default: - ui = NewEphemeralTerminalUI() + ui = NewEphemeralTerminalUI(reportWriter) } return ui From f08ecb5478a586568d667a2dd29f1ea379bab703 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 5 Oct 2021 13:24:54 -0400 Subject: [PATCH 2/6] log errors on close of the report destination Signed-off-by: Alex Goodman --- cmd/packages.go | 6 +++++- cmd/power_user.go | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/cmd/packages.go b/cmd/packages.go index b98062c83f2..85b1db770d7 100644 --- a/cmd/packages.go +++ b/cmd/packages.go @@ -199,7 +199,11 @@ func packagesExec(_ *cobra.Command, args []string) error { userInput := args[0] reporter, closer, err := reportWriter() - defer closer() + defer func() { + if err := closer(); err != nil { + log.Warnf("unable to write to report destination: %+v", err) + } + }() if err != nil { return err diff --git a/cmd/power_user.go b/cmd/power_user.go index bfef9a8ceab..e7c1ff6c5f7 100644 --- a/cmd/power_user.go +++ b/cmd/power_user.go @@ -7,6 +7,7 @@ import ( "github.com/anchore/stereoscope" "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/bus" + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/presenter/poweruser" "github.com/anchore/syft/internal/ui" "github.com/anchore/syft/syft/event" @@ -75,7 +76,11 @@ func powerUserExec(_ *cobra.Command, args []string) error { userInput := args[0] reporter, closer, err := reportWriter() - defer closer() + defer func() { + if err := closer(); err != nil { + log.Warnf("unable to write to report destination: %+v", err) + } + }() if err != nil { return err From 3513dba835fc38e0001899ea34f7e83c408cbff1 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 5 Oct 2021 13:27:46 -0400 Subject: [PATCH 3/6] remove file option from persistent args Signed-off-by: Alex Goodman --- cmd/root.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index d33b47f2a39..cb131972e61 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -40,17 +40,6 @@ func init() { rootCmd.PersistentFlags().CountVarP(&persistentOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)") - rootCmd.PersistentFlags().StringP( - "file", "f", "", - "file to write the report output to (default is STDOUT)", - ) - - flag = "file" - if err := viper.BindPFlag(flag, rootCmd.PersistentFlags().Lookup(flag)); err != nil { - fmt.Printf("unable to bind flag '%s': %+v", flag, err) - os.Exit(1) - } - // set common options that are not universal (package subcommand-alias specific) setPackageFlags(rootCmd.Flags()) } From 2053b6c4f5ae6101882fe3cd3c44e75b103d7cb4 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 5 Oct 2021 13:30:05 -0400 Subject: [PATCH 4/6] update file option comments and logging Signed-off-by: Alex Goodman --- cmd/report_writer.go | 7 ++++++- internal/config/application.go | 2 +- internal/ui/select.go | 3 ++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/cmd/report_writer.go b/cmd/report_writer.go index cb433b9c37c..83d9b923cc9 100644 --- a/cmd/report_writer.go +++ b/cmd/report_writer.go @@ -5,6 +5,8 @@ import ( "io" "os" "strings" + + "github.com/anchore/syft/internal/log" ) func reportWriter() (io.Writer, func() error, error) { @@ -21,7 +23,10 @@ func reportWriter() (io.Writer, func() error, error) { } return reportFile, func() error { if !appConfig.Quiet { - _, _ = fmt.Fprintf(os.Stderr, "Report written to %q\n", path) + _, err = fmt.Fprintf(os.Stderr, "Report written to %q\n", path) + if err != nil { + log.Warnf("unable to write out to stderr the report location: %+v", err) + } } return reportFile.Close() }, nil diff --git a/internal/config/application.go b/internal/config/application.go index 38a86a1cf42..839d7a0e709 100644 --- a/internal/config/application.go +++ b/internal/config/application.go @@ -29,7 +29,7 @@ type parser interface { type Application struct { ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading) Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting - File string `yaml:"file" json:"file" mapstructure:"file"` // -f, the file to write report output to + File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI) CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not Anchore anchore `yaml:"anchore" json:"anchore" mapstructure:"anchore"` // options for interacting with Anchore Engine/Enterprise diff --git a/internal/ui/select.go b/internal/ui/select.go index f3542fc9504..29d32ccd1a8 100644 --- a/internal/ui/select.go +++ b/internal/ui/select.go @@ -11,7 +11,8 @@ import ( // TODO: build tags to exclude options from windows // Select is responsible for determining the specific UI function given select user option, the current platform -// config values, and environment status (such as a TTY being present). +// config values, and environment status (such as a TTY being present). A writer is provided to capture the output +// of the final SBOM report. func Select(verbose, quiet bool, reportWriter io.Writer) UI { var ui UI From ac393daca6bf5c88cfaf7f8aa88d53446684e1c8 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 5 Oct 2021 14:11:29 -0400 Subject: [PATCH 5/6] allow for multiple UI fallback options Signed-off-by: Alex Goodman --- cmd/event_loop.go | 26 ++++++++++++++++---------- cmd/packages.go | 2 +- cmd/power_user.go | 2 +- cmd/report_writer.go | 7 +------ internal/ui/ephemeral_terminal_ui.go | 1 + internal/ui/logger_ui.go | 1 + internal/ui/select.go | 15 ++++++++------- 7 files changed, 29 insertions(+), 25 deletions(-) diff --git a/cmd/event_loop.go b/cmd/event_loop.go index 77f340776b5..95707d071e1 100644 --- a/cmd/event_loop.go +++ b/cmd/event_loop.go @@ -2,6 +2,7 @@ package cmd import ( "errors" + "fmt" "os" "github.com/anchore/syft/internal/log" @@ -14,11 +15,13 @@ import ( // signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until // an eventual graceful exit. // nolint:gocognit,funlen -func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, ux ui.UI, cleanupFn func()) error { +func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error { defer cleanupFn() events := subscription.Events() var err error - if ux, err = setupUI(subscription.Unsubscribe, ux); err != nil { + var ux ui.UI + + if ux, err = setupUI(subscription.Unsubscribe, uxs...); err != nil { return err } @@ -78,15 +81,18 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription * return retErr } -func setupUI(unsubscribe func() error, ux ui.UI) (ui.UI, error) { - if err := ux.Setup(unsubscribe); err != nil { - // replace the existing UI with a (simpler) logger UI - ux = ui.NewLoggerUI(os.Stdout) +// setupUI takes one or more UIs that responds to events and takes a event bus unsubscribe function for use +// during teardown. With the given UIs, the first UI which the ui.Setup() function does not return an error +// will be utilized in execution. Providing a set of UIs allows for the caller to provide graceful fallbacks +// when there are environmental problem (e.g. unable to setup a TUI with the current TTY). +func setupUI(unsubscribe func() error, uis ...ui.UI) (ui.UI, error) { + for _, ux := range uis { if err := ux.Setup(unsubscribe); err != nil { - // something is very wrong, bail. - return ux, err + log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err) + continue } - log.Errorf("unable to setup given UI, falling back to logger: %+v", err) + + return ux, nil } - return ux, nil + return nil, fmt.Errorf("unable to setup any UI") } diff --git a/cmd/packages.go b/cmd/packages.go index 85b1db770d7..4078b66c753 100644 --- a/cmd/packages.go +++ b/cmd/packages.go @@ -213,8 +213,8 @@ func packagesExec(_ *cobra.Command, args []string) error { packagesExecWorker(userInput), setupSignals(), eventSubscription, - ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter), stereoscope.Cleanup, + ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter)..., ) } diff --git a/cmd/power_user.go b/cmd/power_user.go index e7c1ff6c5f7..3b1eacff597 100644 --- a/cmd/power_user.go +++ b/cmd/power_user.go @@ -90,8 +90,8 @@ func powerUserExec(_ *cobra.Command, args []string) error { powerUserExecWorker(userInput), setupSignals(), eventSubscription, - ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter), stereoscope.Cleanup, + ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter)..., ) } diff --git a/cmd/report_writer.go b/cmd/report_writer.go index 83d9b923cc9..bed18fb4f99 100644 --- a/cmd/report_writer.go +++ b/cmd/report_writer.go @@ -22,12 +22,7 @@ func reportWriter() (io.Writer, func() error, error) { return nil, nop, fmt.Errorf("unable to create report file: %w", err) } return reportFile, func() error { - if !appConfig.Quiet { - _, err = fmt.Fprintf(os.Stderr, "Report written to %q\n", path) - if err != nil { - log.Warnf("unable to write out to stderr the report location: %+v", err) - } - } + log.Infof("report written to file=%q", path) return reportFile.Close() }, nil } diff --git a/internal/ui/ephemeral_terminal_ui.go b/internal/ui/ephemeral_terminal_ui.go index ab3063dc7b4..289b34a05ca 100644 --- a/internal/ui/ephemeral_terminal_ui.go +++ b/internal/ui/ephemeral_terminal_ui.go @@ -42,6 +42,7 @@ type ephemeralTerminalUI struct { reportOutput io.Writer } +// NewEphemeralTerminalUI writes all events to a TUI and writes the final report to the given writer. func NewEphemeralTerminalUI(reportWriter io.Writer) UI { return &ephemeralTerminalUI{ handler: ui.NewHandler(), diff --git a/internal/ui/logger_ui.go b/internal/ui/logger_ui.go index 182623699b0..49a8431ad4b 100644 --- a/internal/ui/logger_ui.go +++ b/internal/ui/logger_ui.go @@ -13,6 +13,7 @@ type loggerUI struct { reportOutput io.Writer } +// NewLoggerUI writes all events to the common application logger and writes the final report to the given writer. func NewLoggerUI(reportWriter io.Writer) UI { return &loggerUI{ reportOutput: reportWriter, diff --git a/internal/ui/select.go b/internal/ui/select.go index 29d32ccd1a8..faa9828ca80 100644 --- a/internal/ui/select.go +++ b/internal/ui/select.go @@ -11,10 +11,11 @@ import ( // TODO: build tags to exclude options from windows // Select is responsible for determining the specific UI function given select user option, the current platform -// config values, and environment status (such as a TTY being present). A writer is provided to capture the output -// of the final SBOM report. -func Select(verbose, quiet bool, reportWriter io.Writer) UI { - var ui UI +// config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs +// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there +// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of +// the final SBOM report. +func Select(verbose, quiet bool, reportWriter io.Writer) (uis []UI) { isStdoutATty := terminal.IsTerminal(int(os.Stdout.Fd())) isStderrATty := terminal.IsTerminal(int(os.Stderr.Fd())) @@ -22,10 +23,10 @@ func Select(verbose, quiet bool, reportWriter io.Writer) UI { switch { case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty: - ui = NewLoggerUI(reportWriter) + uis = append(uis, NewLoggerUI(reportWriter)) default: - ui = NewEphemeralTerminalUI(reportWriter) + uis = append(uis, NewEphemeralTerminalUI(reportWriter), NewLoggerUI(reportWriter)) } - return ui + return uis } From ccc083449319c043bfd2b169352d14c9a5d22739 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 5 Oct 2021 14:17:16 -0400 Subject: [PATCH 6/6] update UI select signatures + tests Signed-off-by: Alex Goodman --- cmd/event_loop_test.go | 12 ++++++------ internal/ui/select.go | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/cmd/event_loop_test.go b/cmd/event_loop_test.go index f1c96222176..c816c444efa 100644 --- a/cmd/event_loop_test.go +++ b/cmd/event_loop_test.go @@ -96,8 +96,8 @@ func Test_eventLoop_gracefulExit(t *testing.T) { worker(), signaler(), subscription, - ux, cleanupFn, + ux, ), ) @@ -159,8 +159,8 @@ func Test_eventLoop_workerError(t *testing.T) { worker(), signaler(), subscription, - ux, cleanupFn, + ux, ), workerErr, "should have seen a worker error, but did not", @@ -230,8 +230,8 @@ func Test_eventLoop_unsubscribeError(t *testing.T) { worker(), signaler(), subscription, - ux, cleanupFn, + ux, ), ) @@ -300,8 +300,8 @@ func Test_eventLoop_handlerError(t *testing.T) { worker(), signaler(), subscription, - ux, cleanupFn, + ux, ), finalEvent.Error, "should have seen a event error, but did not", @@ -355,8 +355,8 @@ func Test_eventLoop_signalsStopExecution(t *testing.T) { worker(), signaler(), subscription, - ux, cleanupFn, + ux, ), ) @@ -425,8 +425,8 @@ func Test_eventLoop_uiTeardownError(t *testing.T) { worker(), signaler(), subscription, - ux, cleanupFn, + ux, ), teardownError, "should have seen a UI teardown error, but did not", diff --git a/internal/ui/select.go b/internal/ui/select.go index faa9828ca80..863f1d30d0b 100644 --- a/internal/ui/select.go +++ b/internal/ui/select.go @@ -16,7 +16,6 @@ import ( // are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of // the final SBOM report. func Select(verbose, quiet bool, reportWriter io.Writer) (uis []UI) { - isStdoutATty := terminal.IsTerminal(int(os.Stdout.Fd())) isStderrATty := terminal.IsTerminal(int(os.Stderr.Fd())) notATerminal := !isStderrATty && !isStdoutATty