Fix exotic CPU stub idle-mining guard and document Linux headless screenshots.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Return sane fallback CPU percent after first sample on unknown Unix platforms; add xvfb/scrot docs and stub tests; clear PROBLEMS.md open bugs.
This commit is contained in:
@@ -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. |
|
| **Terminal virtualization** | 400-line DOM cap only; full virtual scrollback deferred. |
|
||||||
| **Vite chunk weight** | `three` + vendor warnings; FleetTopologyMap lazy but heavy first open. |
|
| **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)
|
## UX / visual (unfixed DV)
|
||||||
|
|
||||||
| ID | Issue |
|
| ID | Issue |
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
//go:build linux
|
//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
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -76,5 +88,5 @@ func captureLinuxScreenshotJPEG() ([]byte, error) {
|
|||||||
return os.ReadFile(path)
|
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")
|
||||||
}
|
}
|
||||||
|
|||||||
28
agent/client/screenshot_linux_test.go
Normal file
28
agent/client/screenshot_linux_test.go
Normal file
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
28
agent/client/screenshot_stub_test.go
Normal file
28
agent/client/screenshot_stub_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
package stats
|
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 {
|
func cpuBusyPercentFromDeltas(idleDelta, totalDelta float64) float64 {
|
||||||
if totalDelta <= 0 {
|
if totalDelta <= 0 {
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
30
agent/stats/cpu_common_test.go
Normal file
30
agent/stats/cpu_common_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
package stats
|
package stats
|
||||||
|
|
||||||
func (r *Reporter) SystemCPUPercent() float64 {
|
func (r *Reporter) SystemCPUPercent() float64 {
|
||||||
return 0
|
return r.stubSystemCPUPercent()
|
||||||
}
|
}
|
||||||
|
|||||||
15
agent/stats/cpu_stub_test.go
Normal file
15
agent/stats/cpu_stub_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -306,11 +306,38 @@ Get-Content ".\data-e2e\logs\<agent-id>.log" -Tail 100
|
|||||||
| Defender off | ✅ | N/A (returns error) | N/A |
|
| Defender off | ✅ | N/A (returns error) | N/A |
|
||||||
| systemd / LaunchAgent persistence | — | ✅ | ✅ |
|
| systemd / LaunchAgent persistence | — | ✅ | ✅ |
|
||||||
| Idle-mode mining (CPU % sample) | ✅ | ✅ (fixed: /proc/stat) | ✅ (sysctl kern.cp_time) |
|
| 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.
|
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
|
## References
|
||||||
|
|
||||||
- Automated test phases: `tests/README.md`
|
- Automated test phases: `tests/README.md`
|
||||||
|
|||||||
Reference in New Issue
Block a user