Complete private Monero miner control stack.
Implement Windows agent with RandomX mining and WebSocket fleet reporting, wire dashboard settings into the builder with saved exe paths, and add project README.
This commit is contained in:
253
agent/client/client.go
Normal file
253
agent/client/client.go
Normal file
@@ -0,0 +1,253 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
"crypto-miner-agent/job"
|
||||
"crypto-miner-agent/miner"
|
||||
"crypto-miner-agent/stats"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type AgentClient struct {
|
||||
cfg config.RuntimeConfig
|
||||
conn *websocket.Conn
|
||||
pool *miner.Pool
|
||||
reporter *stats.Reporter
|
||||
startTime time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
agentID string
|
||||
sharesSubmitted int
|
||||
sharesAccepted int
|
||||
}
|
||||
|
||||
func NewAgentClient(cfg config.RuntimeConfig) *AgentClient {
|
||||
return &AgentClient{
|
||||
cfg: cfg,
|
||||
reporter: stats.NewReporter(),
|
||||
startTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AgentClient) Run() error {
|
||||
c.pool = miner.NewPool(c.cfg.Threads, c.submitShare)
|
||||
c.pool.Start()
|
||||
defer c.pool.Stop()
|
||||
|
||||
for {
|
||||
if err := c.connectLoop(); err != nil {
|
||||
log.Printf("[agent] disconnected: %v", err)
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AgentClient) connectLoop() error {
|
||||
wsURL, err := buildWSURL(c.cfg.ServerURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[agent] connecting to %s", wsURL)
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.conn = conn
|
||||
defer conn.Close()
|
||||
|
||||
if err := c.authenticate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
statsStop := make(chan struct{})
|
||||
go c.statsLoop(statsStop)
|
||||
defer close(statsStop)
|
||||
|
||||
for {
|
||||
_, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var msg Message
|
||||
if err := json.Unmarshal(data, &msg); err != nil {
|
||||
continue
|
||||
}
|
||||
c.handleMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AgentClient) authenticate() error {
|
||||
host, cores, memGB := c.reporter.SystemInfo()
|
||||
payload, _ := json.Marshal(AuthPayload{
|
||||
AgentID: c.agentID,
|
||||
Wallet: c.cfg.Wallet,
|
||||
Version: config.Version,
|
||||
Hostname: host,
|
||||
CPUCores: cores,
|
||||
MemoryGB: memGB,
|
||||
Worker: c.cfg.WorkerName,
|
||||
})
|
||||
if err := c.write(Message{Type: "auth", Payload: payload}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, data, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var msg Message
|
||||
if err := json.Unmarshal(data, &msg); err != nil {
|
||||
return err
|
||||
}
|
||||
if msg.Type != "auth_response" {
|
||||
return fmt.Errorf("unexpected message: %s", msg.Type)
|
||||
}
|
||||
var resp AuthResponse
|
||||
if err := json.Unmarshal(msg.Payload, &resp); err != nil {
|
||||
return err
|
||||
}
|
||||
if !resp.Success {
|
||||
return fmt.Errorf("auth failed: %s", resp.Error)
|
||||
}
|
||||
c.agentID = resp.AgentID
|
||||
log.Printf("[agent] authenticated as %s", c.agentID)
|
||||
c.write(Message{Type: "get_job", Payload: json.RawMessage("{}")})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *AgentClient) handleMessage(msg Message) {
|
||||
switch msg.Type {
|
||||
case "new_job":
|
||||
var j job.Job
|
||||
if err := json.Unmarshal(msg.Payload, &j); err != nil {
|
||||
log.Printf("[agent] bad job payload: %v", err)
|
||||
return
|
||||
}
|
||||
if j.Blob == "" {
|
||||
return
|
||||
}
|
||||
log.Printf("[agent] new job %s height=%d", j.ID, j.Height)
|
||||
c.pool.SetJob(&j)
|
||||
case "share_result":
|
||||
var result ShareResult
|
||||
if err := json.Unmarshal(msg.Payload, &result); err != nil {
|
||||
return
|
||||
}
|
||||
if result.Accepted {
|
||||
c.mu.Lock()
|
||||
c.sharesAccepted++
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AgentClient) submitShare(jobID, nonce, hash string) {
|
||||
c.mu.Lock()
|
||||
c.sharesSubmitted++
|
||||
c.mu.Unlock()
|
||||
|
||||
payload, _ := json.Marshal(SharePayload{
|
||||
JobID: jobID,
|
||||
Nonce: nonce,
|
||||
Hash: hash,
|
||||
Worker: c.cfg.WorkerName,
|
||||
})
|
||||
_ = c.write(Message{Type: "submit_share", Payload: payload})
|
||||
}
|
||||
|
||||
func (c *AgentClient) statsLoop(stop <-chan struct{}) {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
var samples []float64
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
hps := c.pool.HashesPerSecond()
|
||||
c.pool.ResetHashCounter()
|
||||
samples = append(samples, hps)
|
||||
if len(samples) > 90 {
|
||||
samples = samples[len(samples)-90:]
|
||||
}
|
||||
|
||||
var avg15s, avg1m, avg15m float64
|
||||
if len(samples) > 0 {
|
||||
avg15s = samples[len(samples)-1]
|
||||
}
|
||||
if len(samples) >= 6 {
|
||||
for _, v := range samples[len(samples)-6:] {
|
||||
avg1m += v
|
||||
}
|
||||
avg1m /= 6
|
||||
} else {
|
||||
avg1m = avg15s
|
||||
}
|
||||
for _, v := range samples {
|
||||
avg15m += v
|
||||
}
|
||||
avg15m /= float64(len(samples))
|
||||
|
||||
cpuPct, memPct := c.reporter.Usage()
|
||||
c.mu.Lock()
|
||||
submitted := c.sharesSubmitted
|
||||
accepted := c.sharesAccepted
|
||||
c.mu.Unlock()
|
||||
|
||||
payload, _ := json.Marshal(StatsPayload{
|
||||
Hashrate15s: avg15s,
|
||||
Hashrate1m: avg1m,
|
||||
Hashrate15m: avg15m,
|
||||
SharesSubmitted: submitted,
|
||||
SharesAccepted: accepted,
|
||||
CPUUsagePct: cpuPct,
|
||||
MemoryUsagePct: memPct,
|
||||
UptimeSeconds: int(time.Since(c.startTime).Seconds()),
|
||||
})
|
||||
_ = c.write(Message{Type: "stats", Payload: payload})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AgentClient) write(msg Message) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.conn == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
return c.conn.WriteJSON(msg)
|
||||
}
|
||||
|
||||
func buildWSURL(serverURL string) (string, error) {
|
||||
u, err := url.Parse(strings.TrimSpace(serverURL))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "https":
|
||||
u.Scheme = "wss"
|
||||
case "http", "":
|
||||
u.Scheme = "ws"
|
||||
case "wss", "ws":
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported server URL scheme: %s", u.Scheme)
|
||||
}
|
||||
if u.Scheme == "" {
|
||||
u.Scheme = "ws"
|
||||
}
|
||||
u.Path = strings.TrimSuffix(u.Path, "/") + "/ws/agent"
|
||||
u.RawQuery = ""
|
||||
u.Fragment = ""
|
||||
return u.String(), nil
|
||||
}
|
||||
63
agent/client/protocol.go
Normal file
63
agent/client/protocol.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package client
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
}
|
||||
|
||||
type AuthPayload struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
Wallet string `json:"wallet"`
|
||||
Version string `json:"version"`
|
||||
Hostname string `json:"hostname"`
|
||||
CPUCores int `json:"cpu_cores"`
|
||||
MemoryGB int `json:"memory_gb"`
|
||||
Worker string `json:"worker_name"`
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
Success bool `json:"success"`
|
||||
AgentID string `json:"agent_id"`
|
||||
Error string `json:"error"`
|
||||
Config struct {
|
||||
Threads int `json:"threads"`
|
||||
Priority int `json:"priority"`
|
||||
} `json:"config"`
|
||||
}
|
||||
|
||||
type Job struct {
|
||||
ID string `json:"job_id"`
|
||||
Height int64 `json:"height"`
|
||||
BlockTemplate string `json:"blocktemplate"`
|
||||
Difficulty int64 `json:"difficulty"`
|
||||
SeedHash string `json:"seed_hash"`
|
||||
Target string `json:"target"`
|
||||
Blob string `json:"blob"`
|
||||
Algo string `json:"algo"`
|
||||
}
|
||||
|
||||
type SharePayload struct {
|
||||
JobID string `json:"job_id"`
|
||||
Nonce string `json:"nonce"`
|
||||
Hash string `json:"hash"`
|
||||
Worker string `json:"worker_name"`
|
||||
}
|
||||
|
||||
type StatsPayload struct {
|
||||
Hashrate15s float64 `json:"hashrate_15s"`
|
||||
Hashrate1m float64 `json:"hashrate_1m"`
|
||||
Hashrate15m float64 `json:"hashrate_15m"`
|
||||
SharesSubmitted int `json:"shares_submitted"`
|
||||
SharesAccepted int `json:"shares_accepted"`
|
||||
CPUUsagePct float64 `json:"cpu_usage_pct"`
|
||||
MemoryUsagePct float64 `json:"memory_usage_pct"`
|
||||
UptimeSeconds int `json:"uptime_seconds"`
|
||||
}
|
||||
|
||||
type ShareResult struct {
|
||||
JobID string `json:"job_id"`
|
||||
Accepted bool `json:"accepted"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
Reference in New Issue
Block a user