package webui import ( "context" "fmt" "io" "log/slog" "os" "os/exec" "path/filepath" "runtime/debug" "sort" "strings" "sync" "syscall" "time" "bee/audit/internal/app" "bee/audit/internal/platform" ) // Task statuses. const ( TaskPending = "pending" TaskRunning = "running" TaskDone = "done" TaskFailed = "failed" TaskCancelled = "cancelled" ) // taskNames maps target → human-readable name for validate (SAT) runs. var taskNames = map[string]string{ "nvidia": "NVIDIA SAT", "nvidia-targeted-stress": "NVIDIA Targeted Stress Validate (dcgmi diag targeted_stress)", "nvidia-bench-perf": "NVIDIA Bee Bench Perf", "nvidia-bench-power": "NVIDIA Bee Bench Power", "nvidia-bench-autotune": "NVIDIA Bee Bench Power Source Autotune", "nvidia-compute": "NVIDIA Max Compute Load (dcgmproftester)", "nvidia-targeted-power": "NVIDIA Targeted Power (dcgmi diag targeted_power)", "nvidia-pulse": "NVIDIA Pulse Test (dcgmi diag pulse_test)", "nvidia-interconnect": "NVIDIA Interconnect Test (NCCL all_reduce_perf)", "nvidia-bandwidth": "NVIDIA Bandwidth Test (NVBandwidth)", "nvidia-stress": "NVIDIA GPU Stress", "memory": "Memory SAT", "storage": "Storage SAT", "tpm": "TPM Read-only Check", "nvidia-config": "GPU Config & NVLink Check", "pcie-link": "PCIe Link Check (forced retrain)", "nvidia-pcie-bandwidth": "NVIDIA GPU PCIe Bandwidth Check", "cpu": "CPU SAT", "amd": "AMD GPU SAT", "amd-mem": "AMD GPU MEM Integrity", "amd-bandwidth": "AMD GPU MEM Bandwidth", "amd-stress": "AMD GPU Burn-in", "memory-stress": "Memory Burn-in", "sat-stress": "SAT Stress (stressapptest)", "platform-stress": "Platform Thermal Cycling", "audit": "Audit", "support-bundle": "Support Bundle", "install": "Install to Disk", "install-to-ram": "Install to RAM", "nvme-format": "NVMe Block Format Change", } // burnNames maps target → human-readable name when a burn profile is set. var burnNames = map[string]string{ "nvidia": "NVIDIA Burn-in", "memory": "Memory Burn-in", "cpu": "CPU Burn-in", "amd": "AMD GPU Burn-in", } func nvidiaStressTaskName(loader string) string { switch strings.TrimSpace(strings.ToLower(loader)) { case platform.NvidiaStressLoaderJohn: return "NVIDIA GPU Stress (John/OpenCL)" case platform.NvidiaStressLoaderNCCL: return "NVIDIA GPU Stress (NCCL)" default: return "NVIDIA GPU Stress (bee-gpu-burn)" } } func taskDisplayName(target, profile, loader string) string { name := taskNames[target] if profile != "" { if n, ok := burnNames[target]; ok { name = n } } if target == "nvidia-stress" { name = nvidiaStressTaskName(loader) } if name == "" { name = target } return name } // Task represents one unit of work in the queue. type Task struct { ID string `json:"id"` Name string `json:"name"` Target string `json:"target"` Priority int `json:"priority"` Status string `json:"status"` CreatedAt time.Time `json:"created_at"` StartedAt *time.Time `json:"started_at,omitempty"` DoneAt *time.Time `json:"done_at,omitempty"` ElapsedSec int `json:"elapsed_sec,omitempty"` ErrMsg string `json:"error,omitempty"` LogPath string `json:"log_path,omitempty"` ArtifactsDir string `json:"artifacts_dir,omitempty"` ReportJSONPath string `json:"report_json_path,omitempty"` ReportHTMLPath string `json:"report_html_path,omitempty"` // runtime fields (not serialised) job *jobState runnerPID int params taskParams } // taskParams holds optional parameters parsed from the run request. type taskParams struct { Duration int `json:"duration,omitempty"` StressMode bool `json:"stress_mode,omitempty"` GPUIndices []int `json:"gpu_indices,omitempty"` ExcludeGPUIndices []int `json:"exclude_gpu_indices,omitempty"` StaggerGPUStart bool `json:"stagger_gpu_start,omitempty"` SizeMB int `json:"size_mb,omitempty"` Passes int `json:"passes,omitempty"` Loader string `json:"loader,omitempty"` BurnProfile string `json:"burn_profile,omitempty"` BenchmarkProfile string `json:"benchmark_profile,omitempty"` BenchmarkKind string `json:"benchmark_kind,omitempty"` RunNCCL bool `json:"run_nccl,omitempty"` ParallelGPUs bool `json:"parallel_gpus,omitempty"` RampStep int `json:"ramp_step,omitempty"` RampTotal int `json:"ramp_total,omitempty"` RampRunID string `json:"ramp_run_id,omitempty"` DisplayName string `json:"display_name,omitempty"` Device string `json:"device,omitempty"` // for install LBAF int `json:"lbaf,omitempty"` PlatformComponents []string `json:"platform_components,omitempty"` SAADmiChanges []saaChange `json:"saa_dmi_changes,omitempty"` FRUChanges []fruChange `json:"fru_changes,omitempty"` HuaweiElabelChanges []huaweiChange `json:"huawei_elabel_changes,omitempty"` RAIDController int `json:"raid_controller,omitempty"` RAIDDevices []string `json:"raid_devices,omitempty"` RAIDArrayName string `json:"raid_array_name,omitempty"` RAIDSlot string `json:"raid_slot,omitempty"` ScenarioName string `json:"scenario_name,omitempty"` } type persistedTask struct { ID string `json:"id"` Name string `json:"name"` Target string `json:"target"` Priority int `json:"priority"` Status string `json:"status"` CreatedAt time.Time `json:"created_at"` StartedAt *time.Time `json:"started_at,omitempty"` DoneAt *time.Time `json:"done_at,omitempty"` ErrMsg string `json:"error,omitempty"` LogPath string `json:"log_path,omitempty"` ArtifactsDir string `json:"artifacts_dir,omitempty"` ReportJSONPath string `json:"report_json_path,omitempty"` ReportHTMLPath string `json:"report_html_path,omitempty"` Params taskParams `json:"params,omitempty"` } type burnPreset struct { DurationSec int } type nvidiaRampSpec struct { DurationSec int StaggerSeconds int TotalDurationSec int } func resolveMemoryValidatePreset(profile string, stress bool) (sizeMB, passes int) { switch strings.TrimSpace(strings.ToLower(profile)) { case "overnight": return 1024, 2 case "acceptance": return 1024, 1 case "smoke": return 256, 1 } if stress { return 512, 1 } return 256, 1 } func taskMayLeaveOrphanWorkers(target string) bool { switch strings.TrimSpace(strings.ToLower(target)) { case "nvidia", "nvidia-targeted-stress", "nvidia-targeted-power", "nvidia-pulse", "nvidia-bandwidth", "nvidia-stress", "nvidia-compute", "nvidia-bench-perf", "memory", "memory-stress", "cpu", "sat-stress", "platform-stress": return true default: return false } } func resolveBurnPreset(profile string) burnPreset { switch profile { case "overnight": return burnPreset{DurationSec: 8 * 60 * 60} case "acceptance": return burnPreset{DurationSec: 60 * 60} default: return burnPreset{DurationSec: 5 * 60} } } func resolveNvidiaRampPlan(profile string, enabled bool, selected []int) (nvidiaRampSpec, error) { base := resolveBurnPreset(profile).DurationSec plan := nvidiaRampSpec{ DurationSec: base, TotalDurationSec: base, } if !enabled { return plan, nil } count := len(selected) if count == 0 { return nvidiaRampSpec{}, fmt.Errorf("staggered NVIDIA burn requires explicit GPU selection") } if count == 1 { return plan, nil } switch profile { case "acceptance": plan.StaggerSeconds = 10 * 60 plan.TotalDurationSec = plan.DurationSec + plan.StaggerSeconds*(count-1) case "overnight": plan.StaggerSeconds = 60 * 60 plan.TotalDurationSec = 8 * 60 * 60 minTotal := count * 60 * 60 if plan.TotalDurationSec < minTotal { plan.TotalDurationSec = minTotal } if plan.TotalDurationSec > 10*60*60 { return nvidiaRampSpec{}, fmt.Errorf("overnight staggered NVIDIA burn supports at most 10 GPUs") } plan.DurationSec = plan.TotalDurationSec - plan.StaggerSeconds*(count-1) default: plan.StaggerSeconds = 2 * 60 plan.TotalDurationSec = plan.DurationSec + plan.StaggerSeconds*(count-1) } return plan, nil } func resolvePlatformStressPreset(profile string) platform.PlatformStressOptions { acceptanceCycles := []platform.PlatformStressCycle{ {LoadSec: 85, IdleSec: 5}, {LoadSec: 80, IdleSec: 10}, {LoadSec: 55, IdleSec: 5}, {LoadSec: 60, IdleSec: 0}, {LoadSec: 100, IdleSec: 10}, {LoadSec: 145, IdleSec: 15}, {LoadSec: 190, IdleSec: 20}, {LoadSec: 235, IdleSec: 25}, {LoadSec: 280, IdleSec: 30}, {LoadSec: 325, IdleSec: 35}, {LoadSec: 370, IdleSec: 40}, {LoadSec: 415, IdleSec: 45}, {LoadSec: 460, IdleSec: 50}, {LoadSec: 510, IdleSec: 0}, } switch profile { case "overnight": cycles := make([]platform.PlatformStressCycle, 0, len(acceptanceCycles)*8) for range 8 { cycles = append(cycles, acceptanceCycles...) } return platform.PlatformStressOptions{Cycles: cycles} case "acceptance": return platform.PlatformStressOptions{Cycles: acceptanceCycles} default: // smoke return platform.PlatformStressOptions{Cycles: []platform.PlatformStressCycle{ {LoadSec: 85, IdleSec: 5}, {LoadSec: 80, IdleSec: 10}, {LoadSec: 55, IdleSec: 5}, {LoadSec: 60, IdleSec: 0}, }} } } // taskQueue manages a priority-ordered list of tasks and runs them one at a time. type taskQueue struct { mu sync.Mutex tasks []*Task trigger chan struct{} opts *HandlerOptions // set by startWorker statePath string logsDir string started bool kmsgWatcher *kmsgWatcher } var globalQueue = &taskQueue{trigger: make(chan struct{}, 1)} const maxTaskHistory = 50 var ( runMemoryAcceptancePackCtx = func(a *app.App, ctx context.Context, baseDir string, sizeMB, passes int, logFunc func(string)) (string, error) { return a.RunMemoryAcceptancePackCtx(ctx, baseDir, sizeMB, passes, logFunc) } runStorageAcceptancePackCtx = func(a *app.App, ctx context.Context, baseDir string, extended bool, logFunc func(string)) (string, error) { return a.RunStorageAcceptancePackCtx(ctx, baseDir, extended, logFunc) } runTPMValidationPackCtx = func(a *app.App, ctx context.Context, baseDir string, logFunc func(string)) (string, error) { return a.RunTPMValidationPackCtx(ctx, baseDir, logFunc) } runNvidiaConfigCheckPackCtx = func(a *app.App, ctx context.Context, baseDir string, logFunc func(string)) (string, error) { return a.RunNvidiaConfigCheckPackCtx(ctx, baseDir, logFunc) } runPCIeLinkCheckPackCtx = func(a *app.App, ctx context.Context, baseDir string, logFunc func(string)) (string, error) { return a.RunPCIeLinkCheckPackCtx(ctx, baseDir, logFunc) } runNvidiaPCIeBandwidthPackCtx = func(a *app.App, ctx context.Context, baseDir string, gpuIndices []int, logFunc func(string)) (string, error) { return a.RunNvidiaPCIeBandwidthPackCtx(ctx, baseDir, gpuIndices, logFunc) } runCPUAcceptancePackCtx = func(a *app.App, ctx context.Context, baseDir string, durationSec int, logFunc func(string)) (string, error) { return a.RunCPUAcceptancePackCtx(ctx, baseDir, durationSec, logFunc) } runAMDAcceptancePackCtx = func(a *app.App, ctx context.Context, baseDir string, logFunc func(string)) (string, error) { return a.RunAMDAcceptancePackCtx(ctx, baseDir, logFunc) } runAMDMemIntegrityPackCtx = func(a *app.App, ctx context.Context, baseDir string, logFunc func(string)) (string, error) { return a.RunAMDMemIntegrityPackCtx(ctx, baseDir, logFunc) } runAMDMemBandwidthPackCtx = func(a *app.App, ctx context.Context, baseDir string, logFunc func(string)) (string, error) { return a.RunAMDMemBandwidthPackCtx(ctx, baseDir, logFunc) } runNvidiaStressPackCtx = func(a *app.App, ctx context.Context, baseDir string, opts platform.NvidiaStressOptions, logFunc func(string)) (string, error) { return a.RunNvidiaStressPackCtx(ctx, baseDir, opts, logFunc) } runAMDStressPackCtx = func(a *app.App, ctx context.Context, baseDir string, durationSec int, logFunc func(string)) (string, error) { return a.RunAMDStressPackCtx(ctx, baseDir, durationSec, logFunc) } runMemoryStressPackCtx = func(a *app.App, ctx context.Context, baseDir string, durationSec int, logFunc func(string)) (string, error) { return a.RunMemoryStressPackCtx(ctx, baseDir, durationSec, logFunc) } runSATStressPackCtx = func(a *app.App, ctx context.Context, baseDir string, durationSec int, logFunc func(string)) (string, error) { return a.RunSATStressPackCtx(ctx, baseDir, durationSec, logFunc) } buildSupportBundle = app.BuildSupportBundle installCommand = func(ctx context.Context, device string, logPath string) *exec.Cmd { return exec.CommandContext(ctx, "bee-install", device, logPath) } externalTaskRunnerCommand = func(exportDir, taskID string) (*exec.Cmd, error) { exe, err := os.Executable() if err != nil { return nil, err } return exec.Command(exe, "bee-worker", "--export-dir", exportDir, "--task-id", taskID), nil } ) // enqueue adds a task to the queue and notifies the worker. func (q *taskQueue) enqueue(t *Task) { q.mu.Lock() q.assignTaskLogPathLocked(t) q.tasks = append(q.tasks, t) q.prune() q.persistLocked() q.mu.Unlock() taskSerialEvent(t, "queued") select { case q.trigger <- struct{}{}: default: } } // prune removes oldest completed tasks beyond maxTaskHistory. func (q *taskQueue) prune() { var done []*Task var active []*Task for _, t := range q.tasks { switch t.Status { case TaskDone, TaskFailed, TaskCancelled: done = append(done, t) default: active = append(active, t) } } if len(done) > maxTaskHistory { done = done[len(done)-maxTaskHistory:] } q.tasks = append(active, done...) } // nextPending returns the highest-priority pending task (nil if none). func (q *taskQueue) nextPending() *Task { for _, t := range q.tasks { if t.Status == TaskRunning { return nil } } var best *Task for _, t := range q.tasks { if t.Status != TaskPending { continue } if best == nil || t.Priority > best.Priority || (t.Priority == best.Priority && t.CreatedAt.Before(best.CreatedAt)) { best = t } } return best } // findByID looks up a task by ID. func (q *taskQueue) findByID(id string) (*Task, bool) { q.mu.Lock() defer q.mu.Unlock() for _, t := range q.tasks { if t.ID == id { return t, true } } return nil, false } // findJob returns the jobState for a task ID (for SSE streaming compatibility). func (q *taskQueue) findJob(id string) (*jobState, bool) { t, ok := q.findByID(id) if !ok || t.job == nil { return nil, false } return t.job, true } type taskStreamSource struct { status string errMsg string logPath string job *jobState } func (q *taskQueue) taskStreamSource(id string) (taskStreamSource, bool) { q.mu.Lock() defer q.mu.Unlock() for _, t := range q.tasks { if t.ID != id { continue } return taskStreamSource{ status: t.Status, errMsg: t.ErrMsg, logPath: t.LogPath, job: t.job, }, true } return taskStreamSource{}, false } func (q *taskQueue) hasActiveTarget(target string) bool { q.mu.Lock() defer q.mu.Unlock() for _, t := range q.tasks { if t.Target != target { continue } if t.Status == TaskPending || t.Status == TaskRunning { return true } } return false } // snapshot returns a copy of all tasks sorted for display with newest tasks first. func (q *taskQueue) snapshot() []Task { q.mu.Lock() defer q.mu.Unlock() out := make([]Task, len(q.tasks)) for i, t := range q.tasks { out[i] = *t out[i].ElapsedSec = taskElapsedSec(&out[i], time.Now()) } sort.SliceStable(out, func(i, j int) bool { if !out[i].CreatedAt.Equal(out[j].CreatedAt) { return out[i].CreatedAt.After(out[j].CreatedAt) } si := statusOrder(out[i].Status) sj := statusOrder(out[j].Status) if si != sj { return si < sj } if out[i].Priority != out[j].Priority { return out[i].Priority > out[j].Priority } return out[i].Name < out[j].Name }) return out } func statusOrder(s string) int { switch s { case TaskRunning: return 0 case TaskPending: return 1 default: return 2 } } // startWorker launches the queue runner goroutine. func (q *taskQueue) startWorker(opts *HandlerOptions) { q.mu.Lock() q.opts = opts q.statePath = filepath.Join(opts.ExportDir, "tasks-state.json") q.logsDir = filepath.Join(opts.ExportDir, "tasks") _ = os.MkdirAll(q.logsDir, 0755) if !q.started { q.loadLocked() q.started = true q.resumeRunningTasksLocked() goRecoverLoop("task worker", 2*time.Second, q.worker) } hasPending := q.nextPending() != nil q.mu.Unlock() if hasPending { select { case q.trigger <- struct{}{}: default: } } } func (q *taskQueue) worker() { for { <-q.trigger func() { setCPUGovernor("performance") defer setCPUGovernor("powersave") for { q.mu.Lock() t := q.nextPending() if t == nil { q.prune() q.persistLocked() q.mu.Unlock() return } now := time.Now() t.Status = TaskRunning t.StartedAt = &now t.DoneAt = nil t.ErrMsg = "" j := newTaskJobState(t.LogPath) t.job = j q.persistLocked() q.mu.Unlock() q.runTaskExternal(t, j) q.mu.Lock() q.prune() q.persistLocked() q.mu.Unlock() } }() } } func (q *taskQueue) resumeRunningTasksLocked() { for _, t := range q.tasks { if t.Status != TaskRunning { continue } if t.job == nil { t.job = newTaskJobState(t.LogPath) } q.attachExternalTaskControlsLocked(t, t.job) q.startRecoveredTaskMonitorLocked(t, t.job) } } func (q *taskQueue) attachExternalTaskControlsLocked(t *Task, j *jobState) { if t == nil || j == nil { return } j.cancel = func() { pid := t.runnerPID if pid <= 0 { if state, ok := readTaskRunnerState(t); ok { pid = state.PID } } if pid > 0 { _ = syscall.Kill(pid, syscall.SIGTERM) } } } func (q *taskQueue) startRecoveredTaskMonitorLocked(t *Task, j *jobState) { if t == nil || j == nil || t.runnerPID <= 0 { return } goRecoverOnce("task runner monitor", func() { stopTail := make(chan struct{}) doneTail := make(chan struct{}) go q.followTaskLog(t, j, stopTail, doneTail) for processAlive(t.runnerPID) { time.Sleep(500 * time.Millisecond) } close(stopTail) <-doneTail q.finishExternalTask(t, j, nil) }) } func (q *taskQueue) runTaskExternal(t *Task, j *jobState) { startedKmsgWatch := false if q.kmsgWatcher != nil && isSATTarget(t.Target) { q.kmsgWatcher.NotifyTaskStarted(t.ID, t.Target) startedKmsgWatch = true } defer func() { if startedKmsgWatch && q.kmsgWatcher != nil { q.kmsgWatcher.NotifyTaskFinished(t.ID) } }() stopTail := make(chan struct{}) doneTail := make(chan struct{}) defer func() { close(stopTail) <-doneTail }() go q.followTaskLog(t, j, stopTail, doneTail) cmd, err := externalTaskRunnerCommand(q.opts.ExportDir, t.ID) if err != nil { j.appendFromLog("ERROR: " + err.Error()) q.finishExternalTask(t, j, err) return } if err := cmd.Start(); err != nil { j.appendFromLog("ERROR: " + err.Error()) q.finishExternalTask(t, j, err) return } q.mu.Lock() t.runnerPID = cmd.Process.Pid q.attachExternalTaskControlsLocked(t, j) q.persistLocked() q.mu.Unlock() waitErr := cmd.Wait() time.Sleep(200 * time.Millisecond) q.finishExternalTask(t, j, waitErr) } func (q *taskQueue) followTaskLog(t *Task, j *jobState, stop <-chan struct{}, done chan<- struct{}) { defer close(done) path := "" if t != nil { path = t.LogPath } if strings.TrimSpace(path) == "" { return } offset := int64(0) if info, err := os.Stat(path); err == nil { offset = info.Size() } var partial string ticker := time.NewTicker(250 * time.Millisecond) defer ticker.Stop() flush := func() { data, newOffset, err := readTaskLogDelta(path, offset) if err != nil || len(data) == 0 { offset = newOffset return } offset = newOffset text := partial + strings.ReplaceAll(string(data), "\r\n", "\n") lines := strings.Split(text, "\n") partial = lines[len(lines)-1] for _, line := range lines[:len(lines)-1] { if line == "" { continue } j.appendFromLog(line) } } for { select { case <-ticker.C: flush() case <-stop: flush() if strings.TrimSpace(partial) != "" { j.appendFromLog(partial) } return } } } func readTaskLogDelta(path string, offset int64) ([]byte, int64, error) { f, err := os.Open(path) if err != nil { return nil, offset, err } defer f.Close() info, err := f.Stat() if err != nil { return nil, offset, err } if info.Size() < offset { offset = 0 } if _, err := f.Seek(offset, io.SeekStart); err != nil { return nil, offset, err } data, err := io.ReadAll(io.LimitReader(f, 1<<20)) return data, offset + int64(len(data)), err } func (q *taskQueue) finishExternalTask(t *Task, j *jobState, waitErr error) { q.mu.Lock() defer q.mu.Unlock() if t.Status == TaskDone || t.Status == TaskFailed || t.Status == TaskCancelled { if j != nil && !j.isDone() { j.finish(t.ErrMsg) j.closeLog() } select { case q.trigger <- struct{}{}: default: } return } state, ok := readTaskRunnerState(t) switch { case ok && state.Status != TaskRunning: t.Status = state.Status t.ErrMsg = state.Error now := state.UpdatedAt if now.IsZero() { now = time.Now() } t.DoneAt = &now case waitErr != nil: now := time.Now() t.Status = TaskFailed t.ErrMsg = waitErr.Error() t.DoneAt = &now default: now := time.Now() t.Status = TaskFailed t.ErrMsg = "task runner exited without final state" t.DoneAt = &now } t.runnerPID = 0 q.finalizeTaskArtifactPathsLocked(t) q.persistLocked() if j != nil && !j.isDone() { j.finish(t.ErrMsg) j.closeLog() } if t.ErrMsg != "" { taskSerialEvent(t, "finished with status="+t.Status+" error="+t.ErrMsg) } else { taskSerialEvent(t, "finished with status="+t.Status) } select { case q.trigger <- struct{}{}: default: } } func (q *taskQueue) executeTask(t *Task, j *jobState, ctx context.Context) { startedKmsgWatch := false defer q.finalizeTaskRun(t, j) defer func() { if startedKmsgWatch && q.kmsgWatcher != nil { q.kmsgWatcher.NotifyTaskFinished(t.ID) } }() defer func() { if rec := recover(); rec != nil { msg := fmt.Sprintf("task panic: %v", rec) slog.Error("task panic", "task_id", t.ID, "target", t.Target, "panic", fmt.Sprint(rec), "stack", string(debug.Stack()), ) j.append("ERROR: " + msg) j.finish(msg) } }() if q.kmsgWatcher != nil && isSATTarget(t.Target) { q.kmsgWatcher.NotifyTaskStarted(t.ID, t.Target) startedKmsgWatch = true } q.runTask(t, j, ctx) } func (q *taskQueue) finalizeTaskRun(t *Task, j *jobState) { q.mu.Lock() now := time.Now() t.DoneAt = &now if t.Status == TaskRunning { if j.err != "" { t.Status = TaskFailed t.ErrMsg = j.err } else { t.Status = TaskDone t.ErrMsg = "" } } q.finalizeTaskArtifactPathsLocked(t) q.persistLocked() q.mu.Unlock() if err := writeTaskReportArtifacts(t); err != nil { appendJobLog(t.LogPath, "WARN: task report generation failed: "+err.Error()) } j.closeLog() if t.ErrMsg != "" { taskSerialEvent(t, "finished with status="+t.Status+" error="+t.ErrMsg) return } taskSerialEvent(t, "finished with status="+t.Status) } // setCPUGovernor writes the given governor to all CPU scaling_governor sysfs files. // Silently ignores errors (e.g. when cpufreq is not available). func setCPUGovernor(governor string) { matches, err := filepath.Glob("/sys/devices/system/cpu/cpu*/cpufreq/scaling_governor") if err != nil || len(matches) == 0 { return } for _, path := range matches { _ = os.WriteFile(path, []byte(governor), 0644) } } // runTask executes the work for a task, writing output to j. It delegates // to executeTaskWithOptions — the exact same target dispatch the external // task runner (RunPersistedTask, see task_runner.go) uses in its own // subprocess — so the in-process path (recovered tasks, unit tests) and the // subprocess path can never drift to different target coverage. Adding a new // task target means editing one switch, not two. func (q *taskQueue) runTask(t *Task, j *jobState, ctx context.Context) { executeTaskWithOptions(q.opts, t, j, ctx) } func splitLines(s string) []string { var out []string for _, l := range splitNL(s) { if l != "" { out = append(out, l) } } return out } func splitNL(s string) []string { var out []string start := 0 for i, c := range s { if c == '\n' { out = append(out, s[start:i]) start = i + 1 } } out = append(out, s[start:]) return out } // ── HTTP handlers ─────────────────────────────────────────────────────────────