mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 16:33:21 +01:00
Split UI from event handling (#448)
* split UI from event handling Signed-off-by: Alex Goodman <wagoodman@gmail.com> * add event loop tests Signed-off-by: Alex Goodman <wagoodman@gmail.com> * use stereoscope cleanup function during signal handling Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * correct error wrapping in packages cmd Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * migrate ui event handlers to ui package Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * clarify command worker input var + remove dead comments Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
parent
706322f826
commit
962e82297c
2
Makefile
2
Makefile
@ -15,7 +15,7 @@ RESET := $(shell tput -T linux sgr0)
|
|||||||
TITLE := $(BOLD)$(PURPLE)
|
TITLE := $(BOLD)$(PURPLE)
|
||||||
SUCCESS := $(BOLD)$(GREEN)
|
SUCCESS := $(BOLD)$(GREEN)
|
||||||
# the quality gate lower threshold for unit test total % coverage (by function statements)
|
# the quality gate lower threshold for unit test total % coverage (by function statements)
|
||||||
COVERAGE_THRESHOLD := 70
|
COVERAGE_THRESHOLD := 65
|
||||||
# CI cache busting values; change these if you want CI to not use previous stored cache
|
# CI cache busting values; change these if you want CI to not use previous stored cache
|
||||||
INTEGRATION_CACHE_BUSTER="88738d2f"
|
INTEGRATION_CACHE_BUSTER="88738d2f"
|
||||||
CLI_CACHE_BUSTER="789bacdf"
|
CLI_CACHE_BUSTER="789bacdf"
|
||||||
|
|||||||
90
cmd/event_loop.go
Normal file
90
cmd/event_loop.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
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.
|
||||||
|
// nolint:gocognit
|
||||||
|
func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, ux ui.UI, cleanupFn func()) error {
|
||||||
|
defer cleanupFn()
|
||||||
|
events := subscription.Events()
|
||||||
|
var err error
|
||||||
|
if ux, 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 {
|
||||||
|
// capture the error from the worker and unsubscribe to complete a graceful shutdown
|
||||||
|
retErr = multierror.Append(retErr, err)
|
||||||
|
if err := subscription.Unsubscribe(); err != nil {
|
||||||
|
retErr = multierror.Append(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:
|
||||||
|
// ignore further results from any event source and exit ASAP, but ensure that all cache is cleaned up.
|
||||||
|
// we ignore further errors since cleaning up the tmp directories will affect running catalogers that are
|
||||||
|
// reading/writing from/to their nested temp dirs. This is acceptable since we are bailing without result.
|
||||||
|
|
||||||
|
// TODO: potential future improvement would be to pass context into workers with a cancel function that is
|
||||||
|
// to the event loop. In this way we can have a more controlled shutdown even at the most nested levels
|
||||||
|
// of processing.
|
||||||
|
events = nil
|
||||||
|
workerErrs = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ux.Teardown(); err != nil {
|
||||||
|
retErr = multierror.Append(retErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
if err := ux.Setup(unsubscribe); err != nil {
|
||||||
|
// something is very wrong, bail.
|
||||||
|
return ux, err
|
||||||
|
}
|
||||||
|
log.Errorf("unable to setup given UI, falling back to logger: %+v", err)
|
||||||
|
}
|
||||||
|
return ux, nil
|
||||||
|
}
|
||||||
455
cmd/event_loop_test.go
Normal file
455
cmd/event_loop_test.go
Normal file
@ -0,0 +1,455 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/ui"
|
||||||
|
"github.com/anchore/syft/syft/event"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ ui.UI = (*uiMock)(nil)
|
||||||
|
|
||||||
|
type uiMock struct {
|
||||||
|
t *testing.T
|
||||||
|
finalEvent partybus.Event
|
||||||
|
unsubscribe func() error
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *uiMock) Setup(unsubscribe func() error) error {
|
||||||
|
u.t.Logf("UI Setup called")
|
||||||
|
u.unsubscribe = unsubscribe
|
||||||
|
return u.Called(unsubscribe).Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *uiMock) Handle(event partybus.Event) error {
|
||||||
|
u.t.Logf("UI Handle called: %+v", event.Type)
|
||||||
|
if event == u.finalEvent {
|
||||||
|
assert.NoError(u.t, u.unsubscribe())
|
||||||
|
}
|
||||||
|
return u.Called(event).Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *uiMock) Teardown() error {
|
||||||
|
u.t.Logf("UI Teardown called")
|
||||||
|
return u.Called().Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_eventLoop_gracefulExit(t *testing.T) {
|
||||||
|
test := func(t *testing.T) {
|
||||||
|
|
||||||
|
testBus := partybus.NewBus()
|
||||||
|
subscription := testBus.Subscribe()
|
||||||
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
|
finalEvent := partybus.Event{
|
||||||
|
Type: event.PresenterReady,
|
||||||
|
}
|
||||||
|
|
||||||
|
worker := func() <-chan error {
|
||||||
|
ret := make(chan error)
|
||||||
|
go func() {
|
||||||
|
t.Log("worker running")
|
||||||
|
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||||
|
// then close (a partial shutdown).
|
||||||
|
ret <- nil
|
||||||
|
t.Log("worker sent nothing")
|
||||||
|
close(ret)
|
||||||
|
t.Log("worker closed")
|
||||||
|
// do the other half of the shutdown
|
||||||
|
testBus.Publish(finalEvent)
|
||||||
|
t.Log("worker published final event")
|
||||||
|
}()
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
signaler := func() <-chan os.Signal {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ux := &uiMock{
|
||||||
|
t: t,
|
||||||
|
finalEvent: finalEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the mock sees at least the final event
|
||||||
|
ux.On("Handle", finalEvent).Return(nil)
|
||||||
|
// ensure the mock sees basic setup/teardown events
|
||||||
|
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||||
|
ux.On("Teardown").Return(nil)
|
||||||
|
|
||||||
|
var cleanupCalled bool
|
||||||
|
cleanupFn := func() {
|
||||||
|
t.Log("cleanup called")
|
||||||
|
cleanupCalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t,
|
||||||
|
eventLoop(
|
||||||
|
worker(),
|
||||||
|
signaler(),
|
||||||
|
subscription,
|
||||||
|
ux,
|
||||||
|
cleanupFn,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||||
|
ux.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is a bug, then there is a risk of the event loop never returning
|
||||||
|
testWithTimeout(t, 5*time.Second, test)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_eventLoop_workerError(t *testing.T) {
|
||||||
|
test := func(t *testing.T) {
|
||||||
|
|
||||||
|
testBus := partybus.NewBus()
|
||||||
|
subscription := testBus.Subscribe()
|
||||||
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
|
workerErr := fmt.Errorf("worker error")
|
||||||
|
|
||||||
|
worker := func() <-chan error {
|
||||||
|
ret := make(chan error)
|
||||||
|
go func() {
|
||||||
|
t.Log("worker running")
|
||||||
|
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||||
|
// then close (a partial shutdown).
|
||||||
|
ret <- nil
|
||||||
|
t.Log("worker sent nothing")
|
||||||
|
ret <- workerErr
|
||||||
|
t.Log("worker sent error")
|
||||||
|
close(ret)
|
||||||
|
t.Log("worker closed")
|
||||||
|
// note: NO final event is fired
|
||||||
|
}()
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
signaler := func() <-chan os.Signal {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ux := &uiMock{
|
||||||
|
t: t,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the mock sees basic setup/teardown events
|
||||||
|
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||||
|
ux.On("Teardown").Return(nil)
|
||||||
|
|
||||||
|
var cleanupCalled bool
|
||||||
|
cleanupFn := func() {
|
||||||
|
t.Log("cleanup called")
|
||||||
|
cleanupCalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure we see an error returned
|
||||||
|
assert.ErrorIs(t,
|
||||||
|
eventLoop(
|
||||||
|
worker(),
|
||||||
|
signaler(),
|
||||||
|
subscription,
|
||||||
|
ux,
|
||||||
|
cleanupFn,
|
||||||
|
),
|
||||||
|
workerErr,
|
||||||
|
"should have seen a worker error, but did not",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||||
|
ux.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is a bug, then there is a risk of the event loop never returning
|
||||||
|
testWithTimeout(t, 5*time.Second, test)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_eventLoop_unsubscribeError(t *testing.T) {
|
||||||
|
test := func(t *testing.T) {
|
||||||
|
|
||||||
|
testBus := partybus.NewBus()
|
||||||
|
subscription := testBus.Subscribe()
|
||||||
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
|
finalEvent := partybus.Event{
|
||||||
|
Type: event.PresenterReady,
|
||||||
|
}
|
||||||
|
|
||||||
|
worker := func() <-chan error {
|
||||||
|
ret := make(chan error)
|
||||||
|
go func() {
|
||||||
|
t.Log("worker running")
|
||||||
|
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||||
|
// then close (a partial shutdown).
|
||||||
|
ret <- nil
|
||||||
|
t.Log("worker sent nothing")
|
||||||
|
close(ret)
|
||||||
|
t.Log("worker closed")
|
||||||
|
// do the other half of the shutdown
|
||||||
|
testBus.Publish(finalEvent)
|
||||||
|
t.Log("worker published final event")
|
||||||
|
}()
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
signaler := func() <-chan os.Signal {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ux := &uiMock{
|
||||||
|
t: t,
|
||||||
|
finalEvent: finalEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the mock sees at least the final event... note the unsubscribe error here
|
||||||
|
ux.On("Handle", finalEvent).Return(partybus.ErrUnsubscribe)
|
||||||
|
// ensure the mock sees basic setup/teardown events
|
||||||
|
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||||
|
ux.On("Teardown").Return(nil)
|
||||||
|
|
||||||
|
var cleanupCalled bool
|
||||||
|
cleanupFn := func() {
|
||||||
|
t.Log("cleanup called")
|
||||||
|
cleanupCalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsubscribe errors should be handled and ignored, not propagated. We are additionally asserting that
|
||||||
|
// this case is handled as a controlled shutdown (this test should not timeout)
|
||||||
|
assert.NoError(t,
|
||||||
|
eventLoop(
|
||||||
|
worker(),
|
||||||
|
signaler(),
|
||||||
|
subscription,
|
||||||
|
ux,
|
||||||
|
cleanupFn,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||||
|
ux.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is a bug, then there is a risk of the event loop never returning
|
||||||
|
testWithTimeout(t, 5*time.Second, test)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_eventLoop_handlerError(t *testing.T) {
|
||||||
|
test := func(t *testing.T) {
|
||||||
|
|
||||||
|
testBus := partybus.NewBus()
|
||||||
|
subscription := testBus.Subscribe()
|
||||||
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
|
finalEvent := partybus.Event{
|
||||||
|
Type: event.PresenterReady,
|
||||||
|
Error: fmt.Errorf("unable to create presenter"),
|
||||||
|
}
|
||||||
|
|
||||||
|
worker := func() <-chan error {
|
||||||
|
ret := make(chan error)
|
||||||
|
go func() {
|
||||||
|
t.Log("worker running")
|
||||||
|
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||||
|
// then close (a partial shutdown).
|
||||||
|
ret <- nil
|
||||||
|
t.Log("worker sent nothing")
|
||||||
|
close(ret)
|
||||||
|
t.Log("worker closed")
|
||||||
|
// do the other half of the shutdown
|
||||||
|
testBus.Publish(finalEvent)
|
||||||
|
t.Log("worker published final event")
|
||||||
|
}()
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
signaler := func() <-chan os.Signal {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ux := &uiMock{
|
||||||
|
t: t,
|
||||||
|
finalEvent: finalEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the mock sees at least the final event... note the event error is propagated
|
||||||
|
ux.On("Handle", finalEvent).Return(finalEvent.Error)
|
||||||
|
// ensure the mock sees basic setup/teardown events
|
||||||
|
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||||
|
ux.On("Teardown").Return(nil)
|
||||||
|
|
||||||
|
var cleanupCalled bool
|
||||||
|
cleanupFn := func() {
|
||||||
|
t.Log("cleanup called")
|
||||||
|
cleanupCalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle errors SHOULD propagate the event loop. We are additionally asserting that this case is
|
||||||
|
// handled as a controlled shutdown (this test should not timeout)
|
||||||
|
assert.ErrorIs(t,
|
||||||
|
eventLoop(
|
||||||
|
worker(),
|
||||||
|
signaler(),
|
||||||
|
subscription,
|
||||||
|
ux,
|
||||||
|
cleanupFn,
|
||||||
|
),
|
||||||
|
finalEvent.Error,
|
||||||
|
"should have seen a event error, but did not",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||||
|
ux.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is a bug, then there is a risk of the event loop never returning
|
||||||
|
testWithTimeout(t, 5*time.Second, test)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_eventLoop_signalsStopExecution(t *testing.T) {
|
||||||
|
test := func(t *testing.T) {
|
||||||
|
|
||||||
|
testBus := partybus.NewBus()
|
||||||
|
subscription := testBus.Subscribe()
|
||||||
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
|
worker := func() <-chan error {
|
||||||
|
// the worker will never return work and the event loop will always be waiting...
|
||||||
|
return make(chan error)
|
||||||
|
}
|
||||||
|
|
||||||
|
signaler := func() <-chan os.Signal {
|
||||||
|
ret := make(chan os.Signal)
|
||||||
|
go func() {
|
||||||
|
ret <- syscall.SIGINT
|
||||||
|
// note: we do NOT close the channel to ensure the event loop does not depend on that behavior to exit
|
||||||
|
}()
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
ux := &uiMock{
|
||||||
|
t: t,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the mock sees basic setup/teardown events
|
||||||
|
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||||
|
ux.On("Teardown").Return(nil)
|
||||||
|
|
||||||
|
var cleanupCalled bool
|
||||||
|
cleanupFn := func() {
|
||||||
|
t.Log("cleanup called")
|
||||||
|
cleanupCalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t,
|
||||||
|
eventLoop(
|
||||||
|
worker(),
|
||||||
|
signaler(),
|
||||||
|
subscription,
|
||||||
|
ux,
|
||||||
|
cleanupFn,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||||
|
ux.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is a bug, then there is a risk of the event loop never returning
|
||||||
|
testWithTimeout(t, 5*time.Second, test)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_eventLoop_uiTeardownError(t *testing.T) {
|
||||||
|
test := func(t *testing.T) {
|
||||||
|
|
||||||
|
testBus := partybus.NewBus()
|
||||||
|
subscription := testBus.Subscribe()
|
||||||
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
|
finalEvent := partybus.Event{
|
||||||
|
Type: event.PresenterReady,
|
||||||
|
}
|
||||||
|
|
||||||
|
worker := func() <-chan error {
|
||||||
|
ret := make(chan error)
|
||||||
|
go func() {
|
||||||
|
t.Log("worker running")
|
||||||
|
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||||
|
// then close (a partial shutdown).
|
||||||
|
ret <- nil
|
||||||
|
t.Log("worker sent nothing")
|
||||||
|
close(ret)
|
||||||
|
t.Log("worker closed")
|
||||||
|
// do the other half of the shutdown
|
||||||
|
testBus.Publish(finalEvent)
|
||||||
|
t.Log("worker published final event")
|
||||||
|
}()
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
signaler := func() <-chan os.Signal {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ux := &uiMock{
|
||||||
|
t: t,
|
||||||
|
finalEvent: finalEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
teardownError := fmt.Errorf("sorry, dave, the UI doesn't want to be torn down")
|
||||||
|
|
||||||
|
// ensure the mock sees at least the final event... note the event error is propagated
|
||||||
|
ux.On("Handle", finalEvent).Return(nil)
|
||||||
|
// ensure the mock sees basic setup/teardown events
|
||||||
|
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||||
|
ux.On("Teardown").Return(teardownError)
|
||||||
|
|
||||||
|
var cleanupCalled bool
|
||||||
|
cleanupFn := func() {
|
||||||
|
t.Log("cleanup called")
|
||||||
|
cleanupCalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure we see an error returned
|
||||||
|
assert.ErrorIs(t,
|
||||||
|
eventLoop(
|
||||||
|
worker(),
|
||||||
|
signaler(),
|
||||||
|
subscription,
|
||||||
|
ux,
|
||||||
|
cleanupFn,
|
||||||
|
),
|
||||||
|
teardownError,
|
||||||
|
"should have seen a UI teardown error, but did not",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||||
|
ux.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is a bug, then there is a risk of the event loop never returning
|
||||||
|
testWithTimeout(t, 5*time.Second, test)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWithTimeout(t *testing.T, timeout time.Duration, test func(*testing.T)) {
|
||||||
|
done := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
test(t)
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(timeout):
|
||||||
|
t.Fatal("test timed out")
|
||||||
|
case <-done:
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,10 +6,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/presenter/packages"
|
"github.com/anchore/stereoscope"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/anchore"
|
"github.com/anchore/syft/internal/anchore"
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
@ -19,10 +16,12 @@ import (
|
|||||||
"github.com/anchore/syft/syft/distro"
|
"github.com/anchore/syft/syft/distro"
|
||||||
"github.com/anchore/syft/syft/event"
|
"github.com/anchore/syft/syft/event"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/presenter/packages"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
"github.com/pkg/profile"
|
"github.com/pkg/profile"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/spf13/viper"
|
||||||
"github.com/wagoodman/go-partybus"
|
"github.com/wagoodman/go-partybus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -186,9 +185,15 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func packagesExec(_ *cobra.Command, args []string) error {
|
func packagesExec(_ *cobra.Command, args []string) error {
|
||||||
errs := packagesExecWorker(args[0])
|
// could be an image or a directory, with or without a scheme
|
||||||
ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet)
|
userInput := args[0]
|
||||||
return ux(errs, eventSubscription)
|
return eventLoop(
|
||||||
|
packagesExecWorker(userInput),
|
||||||
|
setupSignals(),
|
||||||
|
eventSubscription,
|
||||||
|
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet),
|
||||||
|
stereoscope.Cleanup,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func packagesExecWorker(userInput string) <-chan error {
|
func packagesExecWorker(userInput string) <-chan error {
|
||||||
@ -200,14 +205,14 @@ func packagesExecWorker(userInput string) <-chan error {
|
|||||||
|
|
||||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
|
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs <- fmt.Errorf("failed to determine image source: %+v", err)
|
errs <- fmt.Errorf("failed to determine image source: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
catalog, d, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
|
catalog, d, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs <- fmt.Errorf("failed to catalog input: %+v", err)
|
errs <- fmt.Errorf("failed to catalog input: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,7 +266,7 @@ func runPackageSbomUpload(src source.Source, s source.Metadata, catalog *pkg.Cat
|
|||||||
Password: appConfig.Anchore.Password,
|
Password: appConfig.Anchore.Password,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create anchore client: %+v", err)
|
return fmt.Errorf("failed to create anchore client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
importCfg := anchore.ImportConfig{
|
importCfg := anchore.ImportConfig{
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/anchore/stereoscope"
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/presenter/poweruser"
|
"github.com/anchore/syft/internal/presenter/poweruser"
|
||||||
"github.com/anchore/syft/internal/ui"
|
"github.com/anchore/syft/internal/ui"
|
||||||
@ -63,9 +63,15 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func powerUserExec(_ *cobra.Command, args []string) error {
|
func powerUserExec(_ *cobra.Command, args []string) error {
|
||||||
errs := powerUserExecWorker(args[0])
|
// could be an image or a directory, with or without a scheme
|
||||||
ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet)
|
userInput := args[0]
|
||||||
return ux(errs, eventSubscription)
|
return eventLoop(
|
||||||
|
powerUserExecWorker(userInput),
|
||||||
|
setupSignals(),
|
||||||
|
eventSubscription,
|
||||||
|
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet),
|
||||||
|
stereoscope.Cleanup,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func powerUserExecWorker(userInput string) <-chan error {
|
func powerUserExecWorker(userInput string) <-chan error {
|
||||||
|
|||||||
20
cmd/signals.go
Normal file
20
cmd/signals.go
Normal 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
|
||||||
|
}
|
||||||
5
go.mod
5
go.mod
@ -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
|
||||||
@ -35,8 +36,8 @@ require (
|
|||||||
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905
|
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905
|
||||||
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.7.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
|
||||||
|
|||||||
8
go.sum
8
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 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=
|
||||||
@ -703,6 +704,7 @@ github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
|
|||||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
@ -710,8 +712,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||||||
github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
|
|
||||||
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
||||||
@ -744,8 +747,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=
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
|
||||||
"github.com/wagoodman/go-partybus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CatalogerPresenterReady is a UI function for processing the CatalogerFinished bus event, displaying the catalog
|
|
||||||
// via the given presenter to stdout.
|
|
||||||
func CatalogerPresenterReady(event partybus.Event) 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 {
|
|
||||||
return fmt.Errorf("unable to show package catalog report: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: move me to a common module (used in multiple repos)
|
|
||||||
|
|
||||||
const (
|
|
||||||
SpinnerCircleOutlineSet = "◜◠◯◎◉●◉◎◯◡◞"
|
|
||||||
SpinnerCircleSet = "◌◯◎◉●◉◎◯"
|
|
||||||
SpinnerDotSet = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
||||||
SpinnerHorizontalBarSet = "▉▊▋▌▍▎▏▎▍▌▋▊▉"
|
|
||||||
SpinnerVerticalBarSet = "▁▃▄▅▆▇█▇▆▅▄▃▁"
|
|
||||||
SpinnerDoubleBarSet = "▁▂▃▄▅▆▇█▉▊▋▌▍▎▏▏▎▍▌▋▊▉█▇▆▅▄▃▂▁"
|
|
||||||
SpinnerArrowSet = "←↖↑↗→↘↓↙"
|
|
||||||
)
|
|
||||||
|
|
||||||
var SpinnerCircleDotSet = []string{
|
|
||||||
"⠈⠁",
|
|
||||||
"⠈⠑",
|
|
||||||
"⠈⠱",
|
|
||||||
"⠈⡱",
|
|
||||||
"⢀⡱",
|
|
||||||
"⢄⡱",
|
|
||||||
"⢄⡱",
|
|
||||||
"⢆⡱",
|
|
||||||
"⢎⡱",
|
|
||||||
"⢎⡰",
|
|
||||||
"⢎⡠",
|
|
||||||
"⢎⡀",
|
|
||||||
"⢎⠁",
|
|
||||||
"⠎⠁",
|
|
||||||
"⠊⠁",
|
|
||||||
}
|
|
||||||
|
|
||||||
type Spinner struct {
|
|
||||||
index int
|
|
||||||
charset []string
|
|
||||||
lock sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSpinner(charset string) Spinner {
|
|
||||||
return Spinner{
|
|
||||||
charset: strings.Split(charset, ""),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSpinnerFromSlice(charset []string) Spinner {
|
|
||||||
return Spinner{
|
|
||||||
charset: charset,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Spinner) Current() string {
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
|
|
||||||
return s.charset[s.index]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Spinner) Next() string {
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
c := s.charset[s.index]
|
|
||||||
s.index++
|
|
||||||
if s.index >= len(s.charset) {
|
|
||||||
s.index = 0
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
42
internal/ui/components/spinner.go
Normal file
42
internal/ui/components/spinner.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: move me to a common module (used in multiple repos)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SpinnerDotSet = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Spinner struct {
|
||||||
|
index int
|
||||||
|
charset []string
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSpinner(charset string) Spinner {
|
||||||
|
return Spinner{
|
||||||
|
charset: strings.Split(charset, ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Spinner) Current() string {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
return s.charset[s.index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Spinner) Next() string {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
c := s.charset[s.index]
|
||||||
|
s.index++
|
||||||
|
if s.index >= len(s.charset) {
|
||||||
|
s.index = 0
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
137
internal/ui/ephemeral_terminal_ui.go
Normal file
137
internal/ui/ephemeral_terminal_ui.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/internal/logger"
|
||||||
|
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 := handleAppUpdateAvailable(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 := handleCatalogerPresenterReady(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
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
49
internal/ui/event_handlers.go
Normal file
49
internal/ui/event_handlers.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
// 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 {
|
||||||
|
return fmt.Errorf("unable to show package catalog report: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAppUpdateAvailable is a UI handler function to display a new application version to the top of the screen.
|
||||||
|
func handleAppUpdateAvailable(_ 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
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
38
internal/ui/logger_ui.go
Normal file
38
internal/ui/logger_ui.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := handleCatalogerPresenterReady(event); err != nil {
|
||||||
|
log.Warnf("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
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -8,11 +8,12 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/ui/components"
|
||||||
|
|
||||||
"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"
|
||||||
@ -22,8 +23,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const maxBarWidth = 50
|
const maxBarWidth = 50
|
||||||
const statusSet = common.SpinnerDotSet // SpinnerCircleOutlineSet
|
const statusSet = components.SpinnerDotSet
|
||||||
const completedStatus = "✔" // "●"
|
const completedStatus = "✔"
|
||||||
const tileFormat = color.Bold
|
const tileFormat = color.Bold
|
||||||
const interval = 150 * time.Millisecond
|
const interval = 150 * time.Millisecond
|
||||||
|
|
||||||
@ -41,14 +42,14 @@ var (
|
|||||||
|
|
||||||
// startProcess is a helper function for providing common elements for long-running UI elements (such as a
|
// startProcess is a helper function for providing common elements for long-running UI elements (such as a
|
||||||
// progress bar formatter and status spinner)
|
// progress bar formatter and status spinner)
|
||||||
func startProcess() (format.Simple, *common.Spinner) {
|
func startProcess() (format.Simple, *components.Spinner) {
|
||||||
width, _ := frame.GetTerminalSize()
|
width, _ := frame.GetTerminalSize()
|
||||||
barWidth := int(0.25 * float64(width))
|
barWidth := int(0.25 * float64(width))
|
||||||
if barWidth > maxBarWidth {
|
if barWidth > maxBarWidth {
|
||||||
barWidth = maxBarWidth
|
barWidth = maxBarWidth
|
||||||
}
|
}
|
||||||
formatter := format.NewSimpleWithTheme(barWidth, format.HeavyNoBarTheme, format.ColorCompleted, format.ColorTodo)
|
formatter := format.NewSimpleWithTheme(barWidth, format.HeavyNoBarTheme, format.ColorCompleted, format.ColorTodo)
|
||||||
spinner := common.NewSpinner(statusSet)
|
spinner := components.NewSpinner(statusSet)
|
||||||
|
|
||||||
return formatter, &spinner
|
return formatter, &spinner
|
||||||
}
|
}
|
||||||
@ -76,7 +77,7 @@ func formatDockerPullPhase(phase docker.PullPhase, inputStr string) string {
|
|||||||
|
|
||||||
// nolint:funlen
|
// nolint:funlen
|
||||||
// formatDockerImagePullStatus writes the docker image pull status summarized into a single line for the given state.
|
// formatDockerImagePullStatus writes the docker image pull status summarized into a single line for the given state.
|
||||||
func formatDockerImagePullStatus(pullStatus *docker.PullStatus, spinner *common.Spinner, line *frame.Line) {
|
func formatDockerImagePullStatus(pullStatus *docker.PullStatus, spinner *components.Spinner, line *frame.Line) {
|
||||||
var size, current uint64
|
var size, current uint64
|
||||||
|
|
||||||
title := tileFormat.Sprint("Pulling image")
|
title := tileFormat.Sprint("Pulling image")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user