split UI from event handling

Signed-off-by: Alex Goodman <wagoodman@gmail.com>
This commit is contained in:
Alex Goodman 2021-06-26 17:23:30 -04:00
parent 706322f826
commit c5390264b0
17 changed files with 328 additions and 301 deletions

77
cmd/event_loop.go Normal file
View File

@ -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
}

View File

@ -186,9 +186,12 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
} }
func packagesExec(_ *cobra.Command, args []string) error { func packagesExec(_ *cobra.Command, args []string) error {
errs := packagesExecWorker(args[0]) return eventLoop(
ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet) packagesExecWorker(args[0]),
return ux(errs, eventSubscription) setupSignals(),
eventSubscription,
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet),
)
} }
func packagesExecWorker(userInput string) <-chan error { func packagesExecWorker(userInput string) <-chan error {

View File

@ -63,9 +63,12 @@ func init() {
} }
func powerUserExec(_ *cobra.Command, args []string) error { func powerUserExec(_ *cobra.Command, args []string) error {
errs := powerUserExecWorker(args[0]) return eventLoop(
ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet) powerUserExecWorker(args[0]),
return ux(errs, eventSubscription) setupSignals(),
eventSubscription,
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet),
)
} }
func powerUserExecWorker(userInput string) <-chan error { func powerUserExecWorker(userInput string) <-chan error {

20
cmd/signals.go Normal file
View File

@ -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
}

3
go.mod
View File

@ -21,6 +21,7 @@ require (
github.com/gookit/color v1.2.7 github.com/gookit/color v1.2.7
github.com/hashicorp/go-multierror v1.1.0 github.com/hashicorp/go-multierror v1.1.0
github.com/hashicorp/go-version v1.2.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/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.3.1 github.com/mitchellh/mapstructure v1.3.1
github.com/olekukonko/tablewriter v0.0.4 github.com/olekukonko/tablewriter v0.0.4
@ -36,7 +37,7 @@ require (
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.7.0 github.com/spf13/viper v1.7.0
github.com/stretchr/testify v1.6.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/go-progress v0.0.0-20200731105512-1020f39e6240
github.com/wagoodman/jotframe v0.0.0-20200730190914-3517092dd163 github.com/wagoodman/jotframe v0.0.0-20200730190914-3517092dd163
github.com/x-cray/logrus-prefixed-formatter v0.5.2 github.com/x-cray/logrus-prefixed-formatter v0.5.2

4
go.sum
View File

@ -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 h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 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/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/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.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 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/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/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/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-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-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 h1:r6BlIP7CVZtMlxUQhT40h1IE1TzEgKVqwmsVGuscvdk=
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA=

View File

@ -1,8 +1,15 @@
package common package common
import ( import (
"context"
"fmt" "fmt"
"io"
"os" "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" syftEventParsers "github.com/anchore/syft/syft/event/parsers"
"github.com/wagoodman/go-partybus" "github.com/wagoodman/go-partybus"
@ -22,3 +29,21 @@ func CatalogerPresenterReady(event partybus.Event) error {
} }
return nil 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
}

View File

@ -8,33 +8,9 @@ import (
// TODO: move me to a common module (used in multiple repos) // TODO: move me to a common module (used in multiple repos)
const ( const (
SpinnerCircleOutlineSet = "◜◠◯◎◉●◉◎◯◡◞"
SpinnerCircleSet = "◌◯◎◉●◉◎◯"
SpinnerDotSet = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" SpinnerDotSet = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
SpinnerHorizontalBarSet = "▉▊▋▌▍▎▏▎▍▌▋▊▉"
SpinnerVerticalBarSet = "▁▃▄▅▆▇█▇▆▅▄▃▁"
SpinnerDoubleBarSet = "▁▂▃▄▅▆▇█▉▊▋▌▍▎▏▏▎▍▌▋▊▉█▇▆▅▄▃▂▁"
SpinnerArrowSet = "←↖↑↗→↘↓↙"
) )
var SpinnerCircleDotSet = []string{
"⠈⠁",
"⠈⠑",
"⠈⠱",
"⠈⡱",
"⢀⡱",
"⢄⡱",
"⢄⡱",
"⢆⡱",
"⢎⡱",
"⢎⡰",
"⢎⡠",
"⢎⡀",
"⢎⠁",
"⠎⠁",
"⠊⠁",
}
type Spinner struct { type Spinner struct {
index int index int
charset []string 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 { func (s *Spinner) Current() string {
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

40
internal/ui/logger_ui.go Normal file
View File

@ -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
}

View File

@ -4,7 +4,6 @@ import (
"os" "os"
"runtime" "runtime"
"github.com/anchore/syft/internal/ui/etui"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
) )
@ -21,9 +20,9 @@ func Select(verbose, quiet bool) UI {
switch { switch {
case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty: case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty:
ui = LoggerUI ui = NewLoggerUI()
default: default:
ui = etui.OutputToEphemeralTUI ui = NewEphemeralTerminalUI()
} }
return ui return ui

View File

@ -4,6 +4,8 @@ import (
"github.com/wagoodman/go-partybus" "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 type UI interface {
// responsible for displaying pertinent events to the user, on the screen or otherwise. Setup(unsubscribe func() error) error
type UI func(<-chan error, *partybus.Subscription) error partybus.Handler
Teardown() error
}

View File

@ -49,9 +49,7 @@ func New(userInput string, registryOptions *image.RegistryOptions) (Source, func
case ImageScheme: case ImageScheme:
img, err := stereoscope.GetImageFromSource(location, imageSource, registryOptions) img, err := stereoscope.GetImageFromSource(location, imageSource, registryOptions)
cleanup := func() { cleanup := stereoscope.Cleanup
stereoscope.Cleanup()
}
if err != nil || img == nil { if err != nil || img == nil {
return Source{}, cleanup, fmt.Errorf("could not fetch image '%s': %w", location, err) return Source{}, cleanup, fmt.Errorf("could not fetch image '%s': %w", location, err)

View File

@ -8,11 +8,12 @@ import (
"sync" "sync"
"time" "time"
"github.com/anchore/syft/internal/ui/common"
"github.com/anchore/stereoscope/pkg/image/docker" "github.com/anchore/stereoscope/pkg/image/docker"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
stereoEventParsers "github.com/anchore/stereoscope/pkg/event/parsers" stereoEventParsers "github.com/anchore/stereoscope/pkg/event/parsers"
"github.com/anchore/syft/internal/ui/common"
syftEventParsers "github.com/anchore/syft/syft/event/parsers" syftEventParsers "github.com/anchore/syft/syft/event/parsers"
"github.com/gookit/color" "github.com/gookit/color"
"github.com/wagoodman/go-partybus" "github.com/wagoodman/go-partybus"