Skip to content

Commit

Permalink
fix: prevent errors from clobbering terminal (#2161)
Browse files Browse the repository at this point in the history
Signed-off-by: Keith Zantow <[email protected]>
  • Loading branch information
kzantow authored Sep 20, 2023
1 parent 58f8c85 commit 7d0d3e1
Show file tree
Hide file tree
Showing 5 changed files with 39 additions and 62 deletions.
8 changes: 5 additions & 3 deletions cmd/syft/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ func create(id clio.Identification) (clio.Application, *cobra.Command) {
return []clio.UI{noUI}, nil
}

h := handler.New(handler.DefaultHandlerConfig())

return []clio.UI{
ui.New(h, false, cfg.Log.Quiet),
ui.New(cfg.Log.Quiet,
handler.New(handler.DefaultHandlerConfig()),
),
noUI,
}, nil
},
Expand All @@ -60,7 +60,9 @@ func create(id clio.Identification) (clio.Application, *cobra.Command) {
// we can hoist them into the internal packages for global use.
stereoscope.SetBus(state.Bus)
bus.Set(state.Bus)

redact.Set(state.RedactStore)

log.Set(state.Logger)
stereoscope.SetLogger(state.Logger)

Expand Down
36 changes: 0 additions & 36 deletions cmd/syft/internal/ui/select.go

This file was deleted.

15 changes: 0 additions & 15 deletions cmd/syft/internal/ui/select_windows.go

This file was deleted.

40 changes: 33 additions & 7 deletions cmd/syft/internal/ui/ui.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package ui

import (
"fmt"
"os"
"sync"
"time"

tea "github.com/charmbracelet/bubbletea"
"github.com/wagoodman/go-partybus"

"github.com/anchore/bubbly"
"github.com/anchore/bubbly/bubbles/frame"
"github.com/anchore/clio"
"github.com/anchore/go-logger"
handler "github.com/anchore/syft/cmd/syft/cli/ui"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/event"
Expand All @@ -29,13 +31,13 @@ type UI struct {
subscription partybus.Unsubscribable
finalizeEvents []partybus.Event

handler *handler.Handler
handler *bubbly.HandlerCollection
frame tea.Model
}

func New(h *handler.Handler, _, quiet bool) *UI {
func New(quiet bool, handlers ...bubbly.EventHandler) *UI {
return &UI{
handler: h,
handler: bubbly.NewHandlerCollection(handlers...),
frame: frame.New(),
running: &sync.WaitGroup{},
quiet: quiet,
Expand Down Expand Up @@ -72,17 +74,27 @@ func (m *UI) Handle(e partybus.Event) error {

func (m *UI) Teardown(force bool) error {
if !force {
m.handler.Running.Wait()
m.handler.Wait()
m.program.Quit()
// typically in all cases we would want to wait for the UI to finish. However there are still error cases
// that are not accounted for, resulting in hangs. For now, we'll just wait for the UI to finish in the
// happy path only. There will always be an indication of the problem to the user via reporting the error
// string from the worker (outside of the UI after teardown).
m.running.Wait()
} else {
_ = runWithTimeout(250*time.Millisecond, func() error {
m.handler.Wait()
return nil
})

// it may be tempting to use Kill() however it has been found that this can cause the terminal to be left in
// a bad state (where Ctrl+C and other control characters no longer works for future processes in that terminal).
m.program.Quit()

_ = runWithTimeout(250*time.Millisecond, func() error {
m.running.Wait()
return nil
})
}

// TODO: allow for writing out the full log output to the screen (only a partial log is shown currently)
Expand Down Expand Up @@ -119,7 +131,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
// today we treat esc and ctrl+c the same, but in the future when the syft worker has a graceful way to
// today we treat esc and ctrl+c the same, but in the future when the worker has a graceful way to
// cancel in-flight work via a context, we can wire up esc to this path with bus.Exit()
case "esc", "ctrl+c":
bus.ExitWithInterrupt()
Expand All @@ -135,7 +147,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.finalizeEvents = append(m.finalizeEvents, msg)

// why not return tea.Quit here for exit events? because there may be UI components that still need the update-render loop.
// for this reason we'll let the syft event loop call Teardown() which will explicitly wait for these components
// for this reason we'll let the event loop call Teardown() which will explicitly wait for these components
return m, nil
}

Expand All @@ -159,3 +171,17 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m UI) View() string {
return m.frame.View()
}

func runWithTimeout(timeout time.Duration, fn func() error) (err error) {
c := make(chan struct{}, 1)
go func() {
err = fn()
c <- struct{}{}
}()
select {
case <-c:
case <-time.After(timeout):
return fmt.Errorf("timed out after %v", timeout)
}
return err
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ require (
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
golang.org/x/mod v0.12.0
golang.org/x/net v0.15.0
golang.org/x/term v0.12.0
golang.org/x/term v0.12.0 // indirect
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.25.0
)
Expand Down

0 comments on commit 7d0d3e1

Please sign in to comment.