diff --git a/audit/internal/tui/forms.go b/audit/internal/tui/forms.go index 91f7110..ee6f856 100644 --- a/audit/internal/tui/forms.go +++ b/audit/internal/tui/forms.go @@ -1,7 +1,6 @@ package tui import ( - "context" "time" "bee/audit/internal/platform" @@ -140,20 +139,7 @@ func (m model) updateConfirm(msg tea.KeyMsg) (tea.Model, tea.Cmd) { pollSATProgress("gpu-amd", since), ) case actionRunFanStress: - m.busyTitle = "GPU Platform Stress Test" - m.progressPrefix = "fan-stress" - m.progressSince = time.Now() - m.progressLines = nil - since := m.progressSince - opts := hcFanStressOpts(m.hcMode, m.app) - return m, tea.Batch( - func() tea.Msg { - ctx := context.Background() - result, err := m.app.RunFanStressTestResult(ctx, opts) - return resultMsg{title: result.Title, body: result.Body, err: err, back: screenHealthCheck} - }, - pollSATProgress("fan-stress", since), - ) + return m.startGPUStressTest() } case "ctrl+c": return m, tea.Quit diff --git a/audit/internal/tui/messages.go b/audit/internal/tui/messages.go index 71873f7..3ee8612 100644 --- a/audit/internal/tui/messages.go +++ b/audit/internal/tui/messages.go @@ -44,3 +44,9 @@ type nvidiaSATDoneMsg struct { body string err error } + +type gpuStressDoneMsg struct { + title string + body string + err error +} diff --git a/audit/internal/tui/screen_health_check.go b/audit/internal/tui/screen_health_check.go index 13ad5e4..aff0ba7 100644 --- a/audit/internal/tui/screen_health_check.go +++ b/audit/internal/tui/screen_health_check.go @@ -3,6 +3,7 @@ package tui import ( "context" "fmt" + "os/exec" "strings" tea "github.com/charmbracelet/bubbletea" @@ -155,6 +156,64 @@ func (m model) hcRunFanStress() (tea.Model, tea.Cmd) { return m, nil } +// startGPUStressTest launches the GPU Platform Stress Test and nvtop concurrently. +// nvtop occupies the full terminal as a live chart; the stress test runs in background. +func (m model) startGPUStressTest() (tea.Model, tea.Cmd) { + opts := hcFanStressOpts(m.hcMode, m.app) + + ctx, cancel := context.WithCancel(context.Background()) + m.gpuStressCancel = cancel + m.gpuStressAborted = false + m.screen = screenGPUStressRunning + m.nvidiaSATCursor = 0 + + stressCmd := func() tea.Msg { + result, err := m.app.RunFanStressTestResult(ctx, opts) + return gpuStressDoneMsg{title: result.Title, body: result.Body, err: err} + } + + nvtopPath, lookErr := exec.LookPath("nvtop") + if lookErr != nil { + return m, stressCmd + } + + return m, tea.Batch( + stressCmd, + tea.ExecProcess(exec.Command(nvtopPath), func(_ error) tea.Msg { + return nvtopClosedMsg{} + }), + ) +} + +// updateGPUStressRunning handles keys on the GPU stress running screen. +func (m model) updateGPUStressRunning(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "o", "O": + nvtopPath, err := exec.LookPath("nvtop") + if err != nil { + return m, nil + } + return m, tea.ExecProcess(exec.Command(nvtopPath), func(_ error) tea.Msg { + return nvtopClosedMsg{} + }) + case "a", "A": + if m.gpuStressCancel != nil { + m.gpuStressCancel() + m.gpuStressCancel = nil + } + m.gpuStressAborted = true + m.screen = screenHealthCheck + m.cursor = 0 + case "ctrl+c": + return m, tea.Quit + } + return m, nil +} + +func renderGPUStressRunning() string { + return "GPU PLATFORM STRESS TEST\n\nTest is running...\n\n[o] Open nvtop [a] Abort test [ctrl+c] quit\n" +} + func (m model) hcRunAll() (tea.Model, tea.Cmd) { for _, sel := range m.hcSel { if sel { diff --git a/audit/internal/tui/types.go b/audit/internal/tui/types.go index 91e18bd..ecdfc25 100644 --- a/audit/internal/tui/types.go +++ b/audit/internal/tui/types.go @@ -27,6 +27,7 @@ const ( screenConfirm screen = "confirm" screenNvidiaSATSetup screen = "nvidia_sat_setup" screenNvidiaSATRunning screen = "nvidia_sat_running" + screenGPUStressRunning screen = "gpu_stress_running" ) type actionKind string @@ -93,6 +94,10 @@ type model struct { nvidiaSATCancel func() nvidiaSATAborted bool + // GPU Platform Stress Test running + gpuStressCancel func() + gpuStressAborted bool + // SAT verbose progress (CPU / Memory / Storage / AMD GPU) progressLines []string progressPrefix string diff --git a/audit/internal/tui/update.go b/audit/internal/tui/update.go index db4f2d8..99b2f3d 100644 --- a/audit/internal/tui/update.go +++ b/audit/internal/tui/update.go @@ -108,6 +108,28 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.handleNvidiaGPUsMsg(msg) case nvtopClosedMsg: return m, nil + case gpuStressDoneMsg: + if m.gpuStressAborted { + return m, nil + } + if m.gpuStressCancel != nil { + m.gpuStressCancel() + m.gpuStressCancel = nil + } + m.prevScreen = screenHealthCheck + m.screen = screenOutput + m.title = msg.title + if msg.err != nil { + body := strings.TrimSpace(msg.body) + if body == "" { + m.body = fmt.Sprintf("ERROR: %v", msg.err) + } else { + m.body = fmt.Sprintf("%s\n\nERROR: %v", body, msg.err) + } + } else { + m.body = msg.body + } + return m, m.refreshSnapshotCmd() case nvidiaSATDoneMsg: if m.nvidiaSATAborted { return m, nil @@ -152,6 +174,8 @@ func (m model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m.updateNvidiaSATSetup(msg) case screenNvidiaSATRunning: return m.updateNvidiaSATRunning(msg) + case screenGPUStressRunning: + return m.updateGPUStressRunning(msg) case screenExportTargets: return m.updateMenu(msg, len(m.targets), m.handleExportTargetsMenu) case screenInterfacePick: diff --git a/audit/internal/tui/view.go b/audit/internal/tui/view.go index 4872d78..f8ab293 100644 --- a/audit/internal/tui/view.go +++ b/audit/internal/tui/view.go @@ -78,6 +78,8 @@ func (m model) View() string { body = renderNvidiaSATSetup(m) case screenNvidiaSATRunning: body = renderNvidiaSATRunning() + case screenGPUStressRunning: + body = renderGPUStressRunning() case screenOutput: body = fmt.Sprintf("%s\n\n%s\n\n[enter/esc] back [ctrl+c] quit\n", m.title, strings.TrimSpace(m.body)) default: