feat(gpu): fix shutdown race, add RVN pool rotation, full test suite. Fix run() deadlock: proc.Kill() on Stop(), dedicated pollStop channel, stopCh checked in retry delay. Add buildPoolList() using RVNBackupPools (was wired in config/UI but unused). 29 new GPU tests covering detection gates, pool URL building, pool rotation, avg(), itoa(), zip extract, mock miner API (T-Rex + TRM), process stop. RVN wallet defaults set. Download URLs verified 200.
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"crypto-miner-agent/config"
|
||||
)
|
||||
|
||||
|
||||
// GPUVendor identifies the discrete GPU brand on the host.
|
||||
type GPUVendor int
|
||||
|
||||
@@ -40,18 +41,27 @@ type GPUMinerStats struct {
|
||||
ActiveAlgo string
|
||||
}
|
||||
|
||||
// rvnEndpoint is one pool entry for the GPU miner (primary or backup).
|
||||
type rvnEndpoint struct {
|
||||
host string
|
||||
port int
|
||||
tls bool
|
||||
pass string
|
||||
}
|
||||
|
||||
// GPUMiner manages one GPU miner sub-process (T-Rex or TeamRedMiner).
|
||||
type GPUMiner struct {
|
||||
cfg config.RuntimeConfig
|
||||
info GPUInfo
|
||||
cfg config.RuntimeConfig
|
||||
info GPUInfo
|
||||
installDir string
|
||||
|
||||
mu sync.RWMutex
|
||||
stats GPUMinerStats
|
||||
active bool
|
||||
mu sync.RWMutex
|
||||
stats GPUMinerStats
|
||||
active bool
|
||||
proc *os.Process // currently running subprocess (nil if stopped)
|
||||
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// newGPUMiner creates a GPUMiner if GPU mining is configured and a supported GPU is detected.
|
||||
@@ -110,15 +120,38 @@ func (g *GPUMiner) GPUModel() string {
|
||||
return g.info.Model
|
||||
}
|
||||
|
||||
// buildPoolList returns the primary pool followed by any configured backups.
|
||||
func (g *GPUMiner) buildPoolList() []rvnEndpoint {
|
||||
eps := []rvnEndpoint{{
|
||||
host: g.cfg.RVNPoolHost,
|
||||
port: g.cfg.RVNPoolPort,
|
||||
tls: g.cfg.RVNPoolTLS,
|
||||
pass: g.cfg.RVNPoolPass,
|
||||
}}
|
||||
for _, bp := range g.cfg.RVNBackupPools {
|
||||
if bp.Host != "" && bp.Port > 0 {
|
||||
eps = append(eps, rvnEndpoint{
|
||||
host: bp.Host,
|
||||
port: bp.Port,
|
||||
tls: bp.TLS,
|
||||
pass: bp.Pass,
|
||||
})
|
||||
}
|
||||
}
|
||||
return eps
|
||||
}
|
||||
|
||||
func (g *GPUMiner) run() {
|
||||
// Ensure the miner binary is present before trying to start.
|
||||
binPath, err := g.ensureMinerBinary()
|
||||
if err != nil {
|
||||
log.Printf("[gpu] could not obtain miner binary: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
retryDelay := 30 * time.Second
|
||||
pools := g.buildPoolList()
|
||||
poolIdx := 0
|
||||
const retryDelay = 30 * time.Second
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-g.stopCh:
|
||||
@@ -126,64 +159,88 @@ func (g *GPUMiner) run() {
|
||||
default:
|
||||
}
|
||||
|
||||
proc, err := g.startProcess(binPath)
|
||||
ep := pools[poolIdx%len(pools)]
|
||||
proc, err := g.startProcessOnPool(binPath, ep)
|
||||
if err != nil {
|
||||
log.Printf("[gpu] failed to start miner process: %v — retry in %s", err, retryDelay)
|
||||
log.Printf("[gpu] failed to start miner: %v — retry in %s (pool %d/%d)", err, retryDelay, poolIdx%len(pools)+1, len(pools))
|
||||
select {
|
||||
case <-g.stopCh:
|
||||
return
|
||||
case <-time.After(retryDelay):
|
||||
continue
|
||||
}
|
||||
poolIdx++
|
||||
continue
|
||||
}
|
||||
|
||||
g.mu.Lock()
|
||||
g.active = true
|
||||
g.proc = proc
|
||||
g.mu.Unlock()
|
||||
|
||||
log.Printf("[gpu] miner started (pid=%d)", proc.Pid)
|
||||
log.Printf("[gpu] %s started (pid=%d) → %s:%d", g.spec().fileName, proc.Pid, ep.host, ep.port)
|
||||
|
||||
// Poll miner API while it runs.
|
||||
// pollStop signals pollStats to exit; closed when this iteration ends.
|
||||
pollStop := make(chan struct{})
|
||||
pollDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(pollDone)
|
||||
g.pollStats()
|
||||
g.pollStats(pollStop)
|
||||
}()
|
||||
|
||||
// Wait for process exit.
|
||||
procState, waitErr := proc.Wait()
|
||||
close(g.stopCh) // signal polling goroutine
|
||||
// Wait for process exit in a goroutine so we can also listen for stop.
|
||||
waitDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, werr := proc.Wait()
|
||||
waitDone <- werr
|
||||
}()
|
||||
|
||||
var stopRequested bool
|
||||
select {
|
||||
case <-g.stopCh:
|
||||
// Agent shutting down — kill the miner process immediately.
|
||||
stopRequested = true
|
||||
_ = proc.Kill()
|
||||
<-waitDone
|
||||
case waitErr := <-waitDone:
|
||||
if waitErr != nil {
|
||||
log.Printf("[gpu] miner exited: %v — rotating to next pool", waitErr)
|
||||
}
|
||||
// Miner crashed or exited cleanly — rotate to next pool on retry.
|
||||
poolIdx++
|
||||
}
|
||||
|
||||
close(pollStop)
|
||||
<-pollDone
|
||||
|
||||
g.mu.Lock()
|
||||
g.active = false
|
||||
g.proc = nil
|
||||
g.mu.Unlock()
|
||||
|
||||
if waitErr != nil {
|
||||
log.Printf("[gpu] miner exited: %v", waitErr)
|
||||
} else if procState != nil && !procState.Success() {
|
||||
log.Printf("[gpu] miner exited with non-zero status: %s", procState)
|
||||
if stopRequested {
|
||||
return
|
||||
}
|
||||
|
||||
// Re-open the stop channel so we can retry cleanly.
|
||||
g.stopCh = make(chan struct{})
|
||||
|
||||
// Wait before retrying, but exit cleanly if Stop() is called.
|
||||
select {
|
||||
case <-g.stopCh:
|
||||
return
|
||||
case <-time.After(retryDelay):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GPUMiner) pollStats() {
|
||||
// pollStats polls the miner's HTTP API until stop is closed.
|
||||
func (g *GPUMiner) pollStats(stop <-chan struct{}) {
|
||||
apiPort := g.apiPort()
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
samples := make([]float64, 0, 90) // 15 min worth at 10s intervals
|
||||
samples := make([]float64, 0, 90) // 15 min at 10s intervals
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-g.stopCh:
|
||||
case <-stop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
hr, tempC, usage, err := fetchMinerStats(g.info.Vendor, apiPort)
|
||||
@@ -194,15 +251,12 @@ func (g *GPUMiner) pollStats() {
|
||||
if len(samples) > 90 {
|
||||
samples = samples[len(samples)-90:]
|
||||
}
|
||||
avg15s := hr
|
||||
avg1m := avg(samples, 6)
|
||||
avg15m := avg(samples, len(samples))
|
||||
|
||||
g.mu.Lock()
|
||||
g.stats = GPUMinerStats{
|
||||
Hashrate15s: avg15s,
|
||||
Hashrate1m: avg1m,
|
||||
Hashrate15m: avg15m,
|
||||
Hashrate15s: hr,
|
||||
Hashrate1m: avg(samples, 6),
|
||||
Hashrate15m: avg(samples, len(samples)),
|
||||
GPUTempC: tempC,
|
||||
GPUUsagePct: usage,
|
||||
ActiveAlgo: "kawpow",
|
||||
@@ -238,6 +292,15 @@ func (g *GPUMiner) apiPort() int {
|
||||
}
|
||||
}
|
||||
|
||||
// buildPoolURL constructs the stratum URL for a given pool endpoint.
|
||||
func buildPoolURL(ep rvnEndpoint) string {
|
||||
scheme := "stratum+tcp"
|
||||
if ep.tls {
|
||||
scheme = "stratum+ssl"
|
||||
}
|
||||
return fmt.Sprintf("%s://%s:%d", scheme, ep.host, ep.port)
|
||||
}
|
||||
|
||||
// ---- Miner binary management ----
|
||||
|
||||
type minerSpec struct {
|
||||
|
||||
Reference in New Issue
Block a user