Upgrade dashboard, builder, and agent resource controls.
Dark UI with hashrate graphs, inline setting help, percent-based threads/RAM, configurable process name and display modes, and LAN-aware run.bat startup banner.
This commit is contained in:
@@ -39,7 +39,8 @@ func NewAgentClient(cfg config.RuntimeConfig) *AgentClient {
|
||||
}
|
||||
|
||||
func (c *AgentClient) Run() error {
|
||||
c.pool = miner.NewPool(c.cfg.Threads, c.submitShare)
|
||||
threads := c.cfg.EffectiveThreads()
|
||||
c.pool = miner.NewPool(threads, c.cfg, c.reporter, c.submitShare)
|
||||
c.pool.Start()
|
||||
defer c.pool.Stop()
|
||||
|
||||
|
||||
@@ -2,18 +2,21 @@ package config
|
||||
|
||||
import "time"
|
||||
|
||||
// Default stub used for local development builds. The Miner Builder replaces this file.
|
||||
func GetBuiltinConfig() BuiltinConfig {
|
||||
return BuiltinConfig{
|
||||
WorkerName: "dev-worker",
|
||||
ServerURL: "http://127.0.0.1:8989",
|
||||
Wallet: "",
|
||||
Threads: 4,
|
||||
ThreadMode: "percent",
|
||||
ThreadPercent: 75,
|
||||
CPUPriority: "below_normal",
|
||||
MiningMode: "always",
|
||||
DisplayMode: "visible",
|
||||
SilentMode: false,
|
||||
RunAs: "user",
|
||||
AutoStart: false,
|
||||
ProcessName: "CryptoMinerWorker",
|
||||
BuildID: "dev",
|
||||
BuiltAt: time.Now(),
|
||||
PoolHost: "pool.supportxmr.com",
|
||||
@@ -21,6 +24,7 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
PoolTLS: true,
|
||||
PoolPass: "x",
|
||||
MaxCPUUsage: 80,
|
||||
MaxMemoryPct: 70,
|
||||
MinFreeRAM: 1024,
|
||||
IdleThresholdPct: 20,
|
||||
IdleDurationMinutes: 5,
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const Version = "1.0.0"
|
||||
|
||||
// BuiltinConfig holds compile-time settings generated by the Miner Builder.
|
||||
type BuiltinConfig struct {
|
||||
WorkerName string
|
||||
ServerURL string
|
||||
Wallet string
|
||||
Threads int
|
||||
ThreadMode string
|
||||
ThreadPercent int
|
||||
CPUPriority string
|
||||
MiningMode string
|
||||
DisplayMode string
|
||||
SilentMode bool
|
||||
RunAs string
|
||||
AutoStart bool
|
||||
ProcessName string
|
||||
BuildID string
|
||||
BuiltAt time.Time
|
||||
PoolHost string
|
||||
@@ -24,6 +29,7 @@ type BuiltinConfig struct {
|
||||
PoolTLS bool
|
||||
PoolPass string
|
||||
MaxCPUUsage int
|
||||
MaxMemoryPct int
|
||||
MinFreeRAM int
|
||||
IdleThresholdPct int
|
||||
IdleDurationMinutes int
|
||||
@@ -31,7 +37,6 @@ type BuiltinConfig struct {
|
||||
ScheduleEnd string
|
||||
}
|
||||
|
||||
// RuntimeConfig is the resolved configuration used by the agent.
|
||||
type RuntimeConfig struct {
|
||||
BuiltinConfig
|
||||
AgentID string
|
||||
@@ -42,15 +47,34 @@ func Load() RuntimeConfig {
|
||||
if b.Threads <= 0 {
|
||||
b.Threads = 4
|
||||
}
|
||||
if b.ThreadMode == "" {
|
||||
b.ThreadMode = "percent"
|
||||
}
|
||||
if b.ThreadPercent <= 0 {
|
||||
b.ThreadPercent = 75
|
||||
}
|
||||
if b.CPUPriority == "" {
|
||||
b.CPUPriority = "below_normal"
|
||||
}
|
||||
if b.MiningMode == "" {
|
||||
b.MiningMode = "always"
|
||||
}
|
||||
if b.DisplayMode == "" {
|
||||
if b.SilentMode {
|
||||
b.DisplayMode = "silent"
|
||||
} else {
|
||||
b.DisplayMode = "visible"
|
||||
}
|
||||
}
|
||||
if b.ProcessName == "" {
|
||||
b.ProcessName = sanitizeProcessName(b.WorkerName)
|
||||
}
|
||||
if b.MaxCPUUsage <= 0 {
|
||||
b.MaxCPUUsage = 80
|
||||
}
|
||||
if b.MaxMemoryPct <= 0 {
|
||||
b.MaxMemoryPct = 70
|
||||
}
|
||||
if b.MinFreeRAM <= 0 {
|
||||
b.MinFreeRAM = 1024
|
||||
}
|
||||
@@ -68,3 +92,58 @@ func Load() RuntimeConfig {
|
||||
}
|
||||
return RuntimeConfig{BuiltinConfig: b}
|
||||
}
|
||||
|
||||
func (c RuntimeConfig) EffectiveThreads() int {
|
||||
mode := strings.ToLower(c.ThreadMode)
|
||||
if mode == "fixed" {
|
||||
if c.Threads < 1 {
|
||||
return 1
|
||||
}
|
||||
return c.Threads
|
||||
}
|
||||
cores := runtime.NumCPU()
|
||||
if cores < 1 {
|
||||
cores = 1
|
||||
}
|
||||
pct := c.ThreadPercent
|
||||
if pct < 1 {
|
||||
pct = 1
|
||||
}
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
threads := int(float64(cores) * float64(pct) / 100.0)
|
||||
if threads < 1 {
|
||||
threads = 1
|
||||
}
|
||||
if threads > cores {
|
||||
threads = cores
|
||||
}
|
||||
return threads
|
||||
}
|
||||
|
||||
func (c RuntimeConfig) HideWindow() bool {
|
||||
switch strings.ToLower(c.DisplayMode) {
|
||||
case "silent", "background":
|
||||
return true
|
||||
default:
|
||||
return c.SilentMode
|
||||
}
|
||||
}
|
||||
|
||||
func (c RuntimeConfig) BackgroundMode() bool {
|
||||
return strings.ToLower(c.DisplayMode) == "background"
|
||||
}
|
||||
|
||||
func sanitizeProcessName(name string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return "CryptoMinerWorker"
|
||||
}
|
||||
replacer := strings.NewReplacer(" ", "", "-", "", "_", "")
|
||||
clean := replacer.Replace(name)
|
||||
if clean == "" {
|
||||
return "CryptoMinerWorker"
|
||||
}
|
||||
return clean
|
||||
}
|
||||
|
||||
45
agent/config/resolve_test.go
Normal file
45
agent/config/resolve_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEffectiveThreadsFixed(t *testing.T) {
|
||||
cfg := RuntimeConfig{BuiltinConfig: BuiltinConfig{ThreadMode: "fixed", Threads: 3}}
|
||||
if got := cfg.EffectiveThreads(); got != 3 {
|
||||
t.Fatalf("expected 3, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveThreadsPercentCaps(t *testing.T) {
|
||||
cfg := RuntimeConfig{BuiltinConfig: BuiltinConfig{ThreadMode: "percent", ThreadPercent: 150}}
|
||||
got := cfg.EffectiveThreads()
|
||||
if got < 1 {
|
||||
t.Fatalf("expected at least 1 thread, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveThreadsPercentZeroUsesMinimum(t *testing.T) {
|
||||
cfg := RuntimeConfig{BuiltinConfig: BuiltinConfig{ThreadMode: "percent", ThreadPercent: 0}}
|
||||
if got := cfg.EffectiveThreads(); got < 1 {
|
||||
t.Fatalf("expected minimum 1 thread, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHideWindowModes(t *testing.T) {
|
||||
silent := RuntimeConfig{BuiltinConfig: BuiltinConfig{DisplayMode: "silent"}}
|
||||
if !silent.HideWindow() {
|
||||
t.Fatal("silent should hide window")
|
||||
}
|
||||
visible := RuntimeConfig{BuiltinConfig: BuiltinConfig{DisplayMode: "visible"}}
|
||||
if visible.HideWindow() {
|
||||
t.Fatal("visible should not hide window")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeProcessName(t *testing.T) {
|
||||
if got := sanitizeProcessName("office pc 1"); got != "officepc1" {
|
||||
t.Fatalf("unexpected process name: %s", got)
|
||||
}
|
||||
if got := sanitizeProcessName(""); got != "CryptoMinerWorker" {
|
||||
t.Fatalf("expected default process name, got %s", got)
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func InstallIfNeeded(cfg config.RuntimeConfig) (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
installedExe := filepath.Join(installDir, "miner.exe")
|
||||
installedExe := filepath.Join(installDir, cfg.ProcessName+".exe")
|
||||
|
||||
if samePath(currentExe, installedExe) {
|
||||
return false, nil
|
||||
|
||||
@@ -31,12 +31,16 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.BackgroundMode() {
|
||||
cfg.CPUPriority = "idle"
|
||||
}
|
||||
|
||||
if err := deploy.SetProcessPriority(cfg.CPUPriority); err != nil {
|
||||
log.Printf("[agent] could not set CPU priority: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[agent] running worker=%s build=%s server=%s threads=%d",
|
||||
cfg.WorkerName, cfg.BuildID, cfg.ServerURL, cfg.Threads)
|
||||
log.Printf("[agent] running worker=%s process=%s build=%s server=%s threads=%d mode=%s display=%s",
|
||||
cfg.WorkerName, cfg.ProcessName, cfg.BuildID, cfg.ServerURL, cfg.EffectiveThreads(), cfg.ThreadMode, cfg.DisplayMode)
|
||||
|
||||
agent := client.NewAgentClient(cfg)
|
||||
if err := agent.Run(); err != nil {
|
||||
|
||||
@@ -8,34 +8,41 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
"crypto-miner-agent/job"
|
||||
"crypto-miner-agent/stats"
|
||||
)
|
||||
|
||||
type ShareHandler func(jobID, nonce, hash string)
|
||||
|
||||
type Pool struct {
|
||||
threads int
|
||||
engine *Engine
|
||||
handler ShareHandler
|
||||
threads int
|
||||
cfg config.RuntimeConfig
|
||||
reporter *stats.Reporter
|
||||
engine *Engine
|
||||
handler ShareHandler
|
||||
|
||||
mu sync.RWMutex
|
||||
currentJob *job.Job
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
paused atomic.Bool
|
||||
|
||||
hashesTotal atomic.Uint64
|
||||
sharesFound atomic.Uint64
|
||||
}
|
||||
|
||||
func NewPool(threads int, handler ShareHandler) *Pool {
|
||||
func NewPool(threads int, cfg config.RuntimeConfig, reporter *stats.Reporter, handler ShareHandler) *Pool {
|
||||
if threads <= 0 {
|
||||
threads = 1
|
||||
}
|
||||
return &Pool{
|
||||
threads: threads,
|
||||
engine: NewEngine(),
|
||||
handler: handler,
|
||||
stopCh: make(chan struct{}),
|
||||
threads: threads,
|
||||
cfg: cfg,
|
||||
reporter: reporter,
|
||||
engine: NewEngine(),
|
||||
handler: handler,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +67,7 @@ func (p *Pool) Start() {
|
||||
p.wg.Add(1)
|
||||
go p.worker(i)
|
||||
}
|
||||
go p.resourceGuard()
|
||||
}
|
||||
|
||||
func (p *Pool) Stop() {
|
||||
@@ -75,6 +83,34 @@ func (p *Pool) ResetHashCounter() {
|
||||
p.hashesTotal.Store(0)
|
||||
}
|
||||
|
||||
func (p *Pool) resourceGuard() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-p.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
p.paused.Store(!p.resourcesOK())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) resourcesOK() bool {
|
||||
freeMB := p.reporter.FreeMemoryMB()
|
||||
if freeMB > 0 && freeMB < uint64(p.cfg.MinFreeRAM) {
|
||||
return false
|
||||
}
|
||||
totalMB := p.reporter.TotalMemoryMB()
|
||||
if totalMB > 0 && p.cfg.MaxMemoryPct > 0 {
|
||||
usedPct := float64(totalMB-freeMB) / float64(totalMB) * 100
|
||||
if usedPct > float64(p.cfg.MaxMemoryPct) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Pool) worker(id int) {
|
||||
defer p.wg.Done()
|
||||
|
||||
@@ -87,6 +123,11 @@ func (p *Pool) worker(id int) {
|
||||
default:
|
||||
}
|
||||
|
||||
if p.paused.Load() {
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
p.mu.RLock()
|
||||
job := p.currentJob
|
||||
p.mu.RUnlock()
|
||||
@@ -101,6 +142,9 @@ func (p *Pool) worker(id int) {
|
||||
return
|
||||
default:
|
||||
}
|
||||
if p.paused.Load() {
|
||||
break
|
||||
}
|
||||
|
||||
hashHex, _, err := p.engine.HashAtNonce(nonce)
|
||||
if err != nil {
|
||||
@@ -126,7 +170,17 @@ func (p *Pool) worker(id int) {
|
||||
|
||||
func uint32ToHex(n uint32) string {
|
||||
b := []byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)}
|
||||
return hex.EncodeToString(b)
|
||||
return hexEncode(b)
|
||||
}
|
||||
|
||||
func hexEncode(b []byte) string {
|
||||
const hexdigits = "0123456789abcdef"
|
||||
out := make([]byte, len(b)*2)
|
||||
for i, v := range b {
|
||||
out[i*2] = hexdigits[v>>4]
|
||||
out[i*2+1] = hexdigits[v&0x0f]
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func difficultyToTargetHex(difficulty int64) string {
|
||||
|
||||
28
agent/miner/target_test.go
Normal file
28
agent/miner/target_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package miner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHashMeetsTargetEqual(t *testing.T) {
|
||||
target := "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
hash := "0000000000000000000000000000000000000000000000000000000000000001"
|
||||
if !hashMeetsTarget(hash, target) {
|
||||
t.Fatal("lower hash should meet high target")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashMeetsTargetReject(t *testing.T) {
|
||||
target := "0000000000000000000000000000000000000000000000000000000000000001"
|
||||
hash := "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
if hashMeetsTarget(hash, target) {
|
||||
t.Fatal("high hash should not meet low target")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDifficultyToTargetHex(t *testing.T) {
|
||||
out := difficultyToTargetHex(1000)
|
||||
if len(out) != 64 {
|
||||
t.Fatalf("expected 64 hex chars, got %d", len(out))
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,25 @@ package stats
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type memoryStatusEx struct {
|
||||
Length uint32
|
||||
MemoryLoad uint32
|
||||
TotalPhys uint64
|
||||
AvailPhys uint64
|
||||
TotalPageFile uint64
|
||||
AvailPageFile uint64
|
||||
TotalVirtual uint64
|
||||
AvailVirtual uint64
|
||||
AvailExtendedVirtual uint64
|
||||
}
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procGlobalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx")
|
||||
)
|
||||
|
||||
type Reporter struct{}
|
||||
@@ -15,21 +33,40 @@ func NewReporter() *Reporter {
|
||||
func (r *Reporter) SystemInfo() (hostname string, cpuCores int, memoryGB int) {
|
||||
hostname, _ = os.Hostname()
|
||||
cpuCores = runtime.NumCPU()
|
||||
memoryGB = 8
|
||||
total, _ := r.memoryStatus()
|
||||
memoryGB = int(total / (1024 * 1024 * 1024))
|
||||
if memoryGB < 1 {
|
||||
memoryGB = 1
|
||||
}
|
||||
return hostname, cpuCores, memoryGB
|
||||
}
|
||||
|
||||
func (r *Reporter) Usage() (cpuPct float64, memPct float64) {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
memPct = float64(m.Alloc) / float64(m.Sys+1) * 100
|
||||
if memPct > 100 {
|
||||
memPct = 100
|
||||
total, avail := r.memoryStatus()
|
||||
if total > 0 {
|
||||
memPct = float64(total-avail) / float64(total) * 100
|
||||
}
|
||||
cpuPct = float64(runtime.NumGoroutine()) // placeholder; Windows perf counters are heavy
|
||||
if cpuPct > 100 {
|
||||
cpuPct = 100
|
||||
}
|
||||
_ = strings.TrimSpace("")
|
||||
// CPU usage is reported by the agent client from mining load; keep a sane default here.
|
||||
cpuPct = 0
|
||||
return cpuPct, memPct
|
||||
}
|
||||
|
||||
func (r *Reporter) FreeMemoryMB() uint64 {
|
||||
_, avail := r.memoryStatus()
|
||||
return avail / (1024 * 1024)
|
||||
}
|
||||
|
||||
func (r *Reporter) TotalMemoryMB() uint64 {
|
||||
total, _ := r.memoryStatus()
|
||||
return total / (1024 * 1024)
|
||||
}
|
||||
|
||||
func (r *Reporter) memoryStatus() (total, avail uint64) {
|
||||
var stat memoryStatusEx
|
||||
stat.Length = uint32(unsafe.Sizeof(stat))
|
||||
ret, _, _ := procGlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&stat)))
|
||||
if ret == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
return stat.TotalPhys, stat.AvailPhys
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user