github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/cmd/syft/cli/ui/handle_attestation.go (about) 1 package ui 2 3 import ( 4 "bufio" 5 "fmt" 6 "io" 7 "strings" 8 "sync" 9 "time" 10 11 tea "github.com/charmbracelet/bubbletea" 12 "github.com/charmbracelet/lipgloss" 13 "github.com/google/uuid" 14 "github.com/wagoodman/go-partybus" 15 "github.com/wagoodman/go-progress" 16 "github.com/zyedidia/generic/queue" 17 18 "github.com/anchore/bubbly/bubbles/taskprogress" 19 "github.com/anchore/syft/internal/log" 20 syftEventParsers "github.com/anchore/syft/syft/event/parsers" 21 ) 22 23 var ( 24 _ tea.Model = (*attestLogFrame)(nil) 25 ) 26 27 type attestLogFrame struct { 28 reader *backgroundLineReader 29 prog progress.Progressable 30 lines []string 31 completed bool 32 failed bool 33 windowSize tea.WindowSizeMsg 34 35 id uint32 36 sequence int 37 38 updateDuration time.Duration 39 borderStype lipgloss.Style 40 } 41 42 // attestLogFrameTickMsg indicates that the timer has ticked and we should render a frame. 43 type attestLogFrameTickMsg struct { 44 Time time.Time 45 Sequence int 46 ID uint32 47 } 48 49 type backgroundLineReader struct { 50 limit int 51 lines *queue.Queue[string] 52 lock *sync.RWMutex 53 54 // This is added specifically for tests to assert when the background reader is done. 55 // The main UI uses the global ui wait group from the handler to otherwise block 56 // Shared concerns among multiple model made it difficult to test using the global wait group 57 // so this is added to allow tests to assert when the background reader is done. 58 running *sync.WaitGroup 59 } 60 61 func (m *Handler) handleAttestationStarted(e partybus.Event) []tea.Model { 62 reader, prog, taskInfo, err := syftEventParsers.ParseAttestationStartedEvent(e) 63 if err != nil { 64 log.WithFields("error", err).Warn("unable to parse event") 65 return nil 66 } 67 68 stage := progress.Stage{} 69 70 tsk := m.newTaskProgress( 71 taskprogress.Title{ 72 Default: taskInfo.Title.Default, 73 Running: taskInfo.Title.WhileRunning, 74 Success: taskInfo.Title.OnSuccess, 75 }, 76 taskprogress.WithStagedProgressable( 77 struct { 78 progress.Progressable 79 progress.Stager 80 }{ 81 Progressable: prog, 82 Stager: &stage, 83 }, 84 ), 85 ) 86 87 tsk.HideStageOnSuccess = false 88 89 if taskInfo.Context != "" { 90 tsk.Context = []string{taskInfo.Context} 91 } 92 93 borderStyle := tsk.HintStyle 94 95 return []tea.Model{ 96 tsk, 97 newLogFrame(newBackgroundLineReader(m.Running, reader, &stage), prog, borderStyle), 98 } 99 } 100 101 func newLogFrame(reader *backgroundLineReader, prog progress.Progressable, borderStyle lipgloss.Style) attestLogFrame { 102 return attestLogFrame{ 103 reader: reader, 104 prog: prog, 105 id: uuid.Must(uuid.NewUUID()).ID(), 106 updateDuration: 250 * time.Millisecond, 107 borderStype: borderStyle, 108 } 109 } 110 111 func newBackgroundLineReader(wg *sync.WaitGroup, reader io.Reader, stage *progress.Stage) *backgroundLineReader { 112 r := &backgroundLineReader{ 113 limit: 7, 114 lock: &sync.RWMutex{}, 115 lines: queue.New[string](), 116 running: &sync.WaitGroup{}, 117 } 118 119 // tracks the background reader for the global handler wait group 120 wg.Add(1) 121 122 // tracks the background reader for the local wait group (used in tests to decouple from the global handler wait group) 123 r.running.Add(1) 124 125 go func() { 126 r.read(reader, stage) 127 wg.Done() 128 r.running.Done() 129 }() 130 131 return r 132 } 133 134 func (l *backgroundLineReader) read(reader io.Reader, stage *progress.Stage) { 135 s := bufio.NewScanner(reader) 136 137 for s.Scan() { 138 l.lock.Lock() 139 140 text := s.Text() 141 l.lines.Enqueue(text) 142 143 if strings.Contains(text, "tlog entry created with index") { 144 fields := strings.SplitN(text, ":", 2) 145 present := text 146 if len(fields) == 2 { 147 present = fmt.Sprintf("transparency log index: %s", fields[1]) 148 } 149 stage.Current = present 150 } else if strings.Contains(text, "WARNING: skipping transparency log upload") { 151 stage.Current = "transparency log upload skipped" 152 } 153 154 // only show the last X lines of the shell output 155 for l.lines.Len() > l.limit { 156 l.lines.Dequeue() 157 } 158 159 l.lock.Unlock() 160 } 161 } 162 163 func (l backgroundLineReader) Lines() []string { 164 l.lock.RLock() 165 defer l.lock.RUnlock() 166 167 var lines []string 168 169 l.lines.Each(func(line string) { 170 lines = append(lines, line) 171 }) 172 173 return lines 174 } 175 176 func (l attestLogFrame) Init() tea.Cmd { 177 // this is the periodic update of state information 178 return func() tea.Msg { 179 return attestLogFrameTickMsg{ 180 // The time at which the tick occurred. 181 Time: time.Now(), 182 183 // The ID of the log frame that this message belongs to. This can be 184 // helpful when routing messages, however bear in mind that log frames 185 // will ignore messages that don't contain ID by default. 186 ID: l.id, 187 188 Sequence: l.sequence, 189 } 190 } 191 } 192 193 func (l attestLogFrame) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 194 switch msg := msg.(type) { 195 case tea.WindowSizeMsg: 196 l.windowSize = msg 197 return l, nil 198 199 case attestLogFrameTickMsg: 200 l.lines = l.reader.Lines() 201 202 l.completed = progress.IsCompleted(l.prog) 203 err := l.prog.Error() 204 l.failed = err != nil && !progress.IsErrCompleted(err) 205 206 tickCmd := l.handleTick(msg) 207 208 return l, tickCmd 209 } 210 211 return l, nil 212 } 213 214 func (l attestLogFrame) View() string { 215 if l.completed && !l.failed { 216 return "" 217 } 218 219 sb := strings.Builder{} 220 221 for _, line := range l.lines { 222 sb.WriteString(fmt.Sprintf(" %s %s\n", l.borderStype.Render("░░"), line)) 223 } 224 225 return sb.String() 226 } 227 228 func (l attestLogFrame) queueNextTick() tea.Cmd { 229 return tea.Tick(l.updateDuration, func(t time.Time) tea.Msg { 230 return attestLogFrameTickMsg{ 231 Time: t, 232 ID: l.id, 233 Sequence: l.sequence, 234 } 235 }) 236 } 237 238 func (l *attestLogFrame) handleTick(msg attestLogFrameTickMsg) tea.Cmd { 239 // If an ID is set, and the ID doesn't belong to this log frame, reject the message. 240 if msg.ID > 0 && msg.ID != l.id { 241 return nil 242 } 243 244 // If a sequence is set, and it's not the one we expect, reject the message. 245 // This prevents the log frame from receiving too many messages and 246 // thus updating too frequently. 247 if msg.Sequence > 0 && msg.Sequence != l.sequence { 248 return nil 249 } 250 251 l.sequence++ 252 253 // note: even if the log is completed we should still respond to stage changes and window size events 254 return l.queueNextTick() 255 }