From f8c997d272af9bfe9efcd140f55c4476064fb496 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Wed, 25 Mar 2026 18:03:45 +0300 Subject: [PATCH] Add missing SAT progress TUI helpers --- audit/internal/tui/sat_progress.go | 131 +++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 audit/internal/tui/sat_progress.go diff --git a/audit/internal/tui/sat_progress.go b/audit/internal/tui/sat_progress.go new file mode 100644 index 0000000..c0c71a3 --- /dev/null +++ b/audit/internal/tui/sat_progress.go @@ -0,0 +1,131 @@ +package tui + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "time" + + "bee/audit/internal/app" + tea "github.com/charmbracelet/bubbletea" +) + +type satProgressMsg struct { + lines []string +} + +// pollSATProgress returns a Cmd that waits 300ms then reads the latest verbose.log +// for the given SAT prefix and returns parsed step progress lines. +func pollSATProgress(prefix string, since time.Time) tea.Cmd { + return tea.Tick(300*time.Millisecond, func(_ time.Time) tea.Msg { + return satProgressMsg{lines: readSATProgressLines(prefix, since)} + }) +} + +func readSATProgressLines(prefix string, since time.Time) []string { + pattern := filepath.Join(app.DefaultSATBaseDir, prefix+"-*/verbose.log") + matches, err := filepath.Glob(pattern) + if err != nil || len(matches) == 0 { + return nil + } + sort.Strings(matches) + // Find the latest file created at or after (since - 5s) to account for clock skew. + cutoff := since.Add(-5 * time.Second) + candidate := "" + for _, m := range matches { + info, statErr := os.Stat(m) + if statErr == nil && info.ModTime().After(cutoff) { + candidate = m + } + } + if candidate == "" { + return nil + } + raw, err := os.ReadFile(candidate) + if err != nil { + return nil + } + return parseSATVerboseProgress(string(raw)) +} + +// parseSATVerboseProgress parses verbose.log content and returns display lines like: +// +// "PASS lscpu (234ms)" +// "FAIL stress-ng (60.0s)" +// "... sensors-after" +func parseSATVerboseProgress(content string) []string { + type step struct { + name string + rc int + durationMs int + done bool + } + + lines := strings.Split(content, "\n") + var steps []step + stepIdx := map[string]int{} + + for i, line := range lines { + line = strings.TrimSpace(line) + if idx := strings.Index(line, "] start "); idx >= 0 { + name := strings.TrimSpace(line[idx+len("] start "):]) + if _, exists := stepIdx[name]; !exists { + stepIdx[name] = len(steps) + steps = append(steps, step{name: name}) + } + } else if idx := strings.Index(line, "] finish "); idx >= 0 { + name := strings.TrimSpace(line[idx+len("] finish "):]) + si, exists := stepIdx[name] + if !exists { + continue + } + steps[si].done = true + for j := i + 1; j < len(lines) && j <= i+3; j++ { + l := strings.TrimSpace(lines[j]) + if strings.HasPrefix(l, "rc: ") { + steps[si].rc, _ = strconv.Atoi(strings.TrimPrefix(l, "rc: ")) + } else if strings.HasPrefix(l, "duration_ms: ") { + steps[si].durationMs, _ = strconv.Atoi(strings.TrimPrefix(l, "duration_ms: ")) + } + } + } + } + + var result []string + for _, s := range steps { + display := cleanSATStepName(s.name) + if s.done { + status := "PASS" + if s.rc != 0 { + status = "FAIL" + } + result = append(result, fmt.Sprintf("%-4s %s (%s)", status, display, fmtDurMs(s.durationMs))) + } else { + result = append(result, fmt.Sprintf("... %s", display)) + } + } + return result +} + +// cleanSATStepName strips leading digits and dash: "01-lscpu.log" → "lscpu". +func cleanSATStepName(name string) string { + name = strings.TrimSuffix(name, ".log") + i := 0 + for i < len(name) && name[i] >= '0' && name[i] <= '9' { + i++ + } + if i < len(name) && name[i] == '-' { + name = name[i+1:] + } + return name +} + +func fmtDurMs(ms int) string { + if ms < 1000 { + return fmt.Sprintf("%dms", ms) + } + return fmt.Sprintf("%.1fs", float64(ms)/1000) +}