Improve fleet control, Crucible ops, and multi-machine identity.
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).
This commit is contained in:
@@ -31,7 +31,7 @@ type Broadcaster func(AlertEvent)
|
||||
type Evaluator struct {
|
||||
db *db.Database
|
||||
thresholds func() Thresholds
|
||||
notify NotifyConfig
|
||||
notify func() NotifyConfig
|
||||
broadcast Broadcaster
|
||||
mu sync.Mutex
|
||||
baseline map[string]float64
|
||||
@@ -40,7 +40,7 @@ type Evaluator struct {
|
||||
cooldown time.Duration
|
||||
}
|
||||
|
||||
func NewEvaluator(database *db.Database, thresholds func() Thresholds, notify NotifyConfig, broadcast Broadcaster) *Evaluator {
|
||||
func NewEvaluator(database *db.Database, thresholds func() Thresholds, notify func() NotifyConfig, broadcast Broadcaster) *Evaluator {
|
||||
return &Evaluator{
|
||||
db: database,
|
||||
thresholds: thresholds,
|
||||
@@ -180,7 +180,7 @@ func (e *Evaluator) fire(ev AlertEvent, cooldownKey string) {
|
||||
e.mu.Unlock()
|
||||
|
||||
log.Printf("[Alert] %s: %s", ev.Type, ev.Message)
|
||||
NotifyAll(e.notify, "AetherForge "+ev.Type, ev.Message)
|
||||
NotifyAll(e.notify(), "AetherForge "+ev.Type, ev.Message)
|
||||
if e.broadcast != nil {
|
||||
e.broadcast(ev)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ func TestEvaluatorOfflineAlert(t *testing.T) {
|
||||
var fired []AlertEvent
|
||||
e := &Evaluator{
|
||||
thresholds: func() Thresholds { return Thresholds{OfflineMinutes: 5} },
|
||||
notify: func() NotifyConfig { return NotifyConfig{} },
|
||||
broadcast: func(ev AlertEvent) { fired = append(fired, ev) },
|
||||
baseline: make(map[string]float64),
|
||||
lastFired: make(map[string]time.Time),
|
||||
|
||||
@@ -207,6 +207,10 @@ func (h *DropperHandler) resolveBase(r *http.Request) string {
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
// Honour X-Forwarded-Proto set by reverse proxies (e.g. Cloudflare tunnel).
|
||||
if proto := r.Header.Get("X-Forwarded-Proto"); proto == "https" {
|
||||
scheme = "https"
|
||||
}
|
||||
// Prefer X-Forwarded-Host (behind a reverse proxy) over the raw Host.
|
||||
host := r.Header.Get("X-Forwarded-Host")
|
||||
if host == "" {
|
||||
|
||||
@@ -417,6 +417,55 @@ func EstimateXMRPerDay(hashrate float64) map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteAgent removes an agent record from the database.
|
||||
// If the agent is currently online it is also disconnected (kicked).
|
||||
func (f *FleetHandler) DeleteAgent(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
if id == "" {
|
||||
http.Error(w, "missing agent id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Kick live connection first (non-fatal if offline).
|
||||
if f.ws != nil {
|
||||
_ = f.ws.SendToAgent(id, Message{Type: "disconnect", Payload: mustMarshalFleet(map[string]string{"reason": "deleted from roster"})})
|
||||
f.ws.RemoveAgent(id)
|
||||
}
|
||||
if err := f.db.DeleteAgent(id); err != nil {
|
||||
http.Error(w, "delete failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
// BulkDeleteAgents deletes multiple agents from the database in one call.
|
||||
func (f *FleetHandler) BulkDeleteAgents(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
IDs []string `json:"ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || len(req.IDs) == 0 {
|
||||
http.Error(w, "ids required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
deleted := 0
|
||||
for _, id := range req.IDs {
|
||||
if f.ws != nil {
|
||||
_ = f.ws.SendToAgent(id, Message{Type: "disconnect", Payload: mustMarshalFleet(map[string]string{"reason": "deleted from roster"})})
|
||||
f.ws.RemoveAgent(id)
|
||||
}
|
||||
if err := f.db.DeleteAgent(id); err == nil {
|
||||
deleted++
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"success": true, "deleted": deleted})
|
||||
}
|
||||
|
||||
func mustMarshalFleet(v interface{}) json.RawMessage {
|
||||
b, _ := json.Marshal(v)
|
||||
return b
|
||||
}
|
||||
|
||||
func parseFloatQuery(r *http.Request, key string, def float64) float64 {
|
||||
v := r.URL.Query().Get(key)
|
||||
if v == "" {
|
||||
|
||||
@@ -182,7 +182,7 @@ func TestFleetGetAlertsWithEvaluator(t *testing.T) {
|
||||
|
||||
evaluator := alerts.NewEvaluator(database, func() alerts.Thresholds {
|
||||
return alerts.Thresholds{OfflineMinutes: 5}
|
||||
}, alerts.NotifyConfig{}, nil)
|
||||
}, func() alerts.NotifyConfig { return alerts.NotifyConfig{} }, nil)
|
||||
evaluator.RunOnce()
|
||||
|
||||
fh := NewFleetHandler(database, NewWSHub(database), NewAIHandler(database), nil, evaluator, pool.Config{})
|
||||
|
||||
@@ -444,7 +444,9 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
r.Post("/agents/{id}/command", fleetHandler.PostAgentCommand)
|
||||
r.Get("/agents/{id}/log", fleetHandler.GetAgentLog)
|
||||
r.Put("/agents/{id}/meta", fleetHandler.PutAgentMeta)
|
||||
r.Delete("/agents/{id}", fleetHandler.DeleteAgent)
|
||||
r.Post("/agents/bulk-command", fleetHandler.PostBulkCommand)
|
||||
r.Post("/agents/bulk-delete", fleetHandler.BulkDeleteAgents)
|
||||
}
|
||||
|
||||
// Fleet ops
|
||||
|
||||
@@ -60,9 +60,13 @@ type Message struct {
|
||||
}
|
||||
|
||||
type AgentConnection struct {
|
||||
AgentID string
|
||||
Conn *websocket.Conn
|
||||
mu sync.Mutex
|
||||
AgentID string
|
||||
Conn *websocket.Conn
|
||||
mu sync.Mutex
|
||||
// Latency tracking — updated each ping/pong cycle.
|
||||
latencyMu sync.Mutex
|
||||
pingSentAt time.Time
|
||||
LatencyMs *int // nil until first pong received
|
||||
}
|
||||
|
||||
func (c *AgentConnection) SendJSON(v interface{}) error {
|
||||
@@ -177,6 +181,35 @@ func (h *WSHub) runPingLoopRaw(conn *websocket.Conn) {
|
||||
}
|
||||
}
|
||||
|
||||
// runPingLoopAgent is like runPingLoopRaw but also records RTT on each pong.
|
||||
func (h *WSHub) runPingLoopAgent(ac *AgentConnection) {
|
||||
interval := h.pingInterval()
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
conn := ac.Conn
|
||||
_ = conn.SetReadDeadline(time.Now().Add(interval * 2))
|
||||
conn.SetPongHandler(func(string) error {
|
||||
// Measure RTT.
|
||||
ac.latencyMu.Lock()
|
||||
if !ac.pingSentAt.IsZero() {
|
||||
ms := int(time.Since(ac.pingSentAt).Milliseconds())
|
||||
ac.LatencyMs = &ms
|
||||
}
|
||||
ac.latencyMu.Unlock()
|
||||
return conn.SetReadDeadline(time.Now().Add(interval * 2))
|
||||
})
|
||||
|
||||
for range ticker.C {
|
||||
ac.latencyMu.Lock()
|
||||
ac.pingSentAt = time.Now()
|
||||
ac.latencyMu.Unlock()
|
||||
if err := conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(10*time.Second)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WSHub) runPingLoopDash(dc *DashboardConn) {
|
||||
interval := h.pingInterval()
|
||||
ticker := time.NewTicker(interval)
|
||||
@@ -263,13 +296,17 @@ func (h *WSHub) getAgentConn(agentID string) *AgentConnection {
|
||||
}
|
||||
|
||||
func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
clientIP := r.Header.Get("X-Forwarded-For")
|
||||
if clientIP == "" {
|
||||
clientIP = r.RemoteAddr
|
||||
}
|
||||
log.Printf("[WS] Agent connection attempt from %s (origin=%s)", clientIP, r.Header.Get("Origin"))
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("WebSocket upgrade error: %v", err)
|
||||
log.Printf("[WS] Agent upgrade failed from %s: %v", clientIP, err)
|
||||
return
|
||||
}
|
||||
|
||||
go h.runPingLoopRaw(conn)
|
||||
log.Printf("[WS] Agent WebSocket upgraded OK from %s", clientIP)
|
||||
|
||||
agentID := ""
|
||||
defer func() {
|
||||
@@ -351,17 +388,18 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify fleet secret. If the server has one configured, the agent must match.
|
||||
h.mu.RLock()
|
||||
requiredSecret := h.fleetSecret
|
||||
h.mu.RUnlock()
|
||||
if requiredSecret != "" && !secureStringEqual(auth.FleetSecret, requiredSecret) {
|
||||
conn.WriteJSON(Message{Type: "auth_response", Payload: mustMarshal(map[string]interface{}{
|
||||
"success": false, "error": "invalid fleet secret — re-forge this agent",
|
||||
})})
|
||||
log.Printf("[auth] Agent rejected: bad fleet secret (host=%s id=%s)", auth.Hostname, auth.AgentID)
|
||||
return
|
||||
}
|
||||
// Verify fleet secret. If the server has one configured, the agent must match.
|
||||
h.mu.RLock()
|
||||
requiredSecret := h.fleetSecret
|
||||
h.mu.RUnlock()
|
||||
log.Printf("[WS] Agent auth: id=%s host=%s secret_prefix=%.8s", auth.AgentID, auth.Hostname, auth.FleetSecret)
|
||||
if requiredSecret != "" && !secureStringEqual(auth.FleetSecret, requiredSecret) {
|
||||
conn.WriteJSON(Message{Type: "auth_response", Payload: mustMarshal(map[string]interface{}{
|
||||
"success": false, "error": "invalid fleet secret — re-forge this agent",
|
||||
})})
|
||||
log.Printf("[auth] Agent rejected: bad fleet secret (host=%s id=%s)", auth.Hostname, auth.AgentID)
|
||||
return
|
||||
}
|
||||
|
||||
agentID = auth.AgentID
|
||||
if agentID == "" {
|
||||
@@ -410,8 +448,6 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Build backup pool.Config list from what the agent sent at auth.
|
||||
// These are registered on the proxy so reconnect() rotates through
|
||||
// them automatically — not just at initial connect.
|
||||
var backupCfgs []pool.Config
|
||||
for _, bp := range backupPools {
|
||||
if bp.Host == "" || bp.Port <= 0 {
|
||||
@@ -431,9 +467,14 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
backupCfgs = append(backupCfgs, bpc)
|
||||
}
|
||||
|
||||
if _, err := h.poolManager.EnsurePoolWithBackups(&poolCfg, backupCfgs); err != nil {
|
||||
log.Printf("[WS] All pools failed for agent %s (%d backups tried) — agent will mine when pool reconnects", agentID, len(backupCfgs))
|
||||
}
|
||||
// Connect to pool in background — do NOT block the auth_response.
|
||||
// The agent can start and the pool proxy will be ready by the time
|
||||
// the first share is submitted.
|
||||
go func(pc pool.Config, bcs []pool.Config, aid string) {
|
||||
if _, err := h.poolManager.EnsurePoolWithBackups(&pc, bcs); err != nil {
|
||||
log.Printf("[WS] Pool init for agent %s failed (will retry): %v", aid, err)
|
||||
}
|
||||
}(poolCfg, backupCfgs, agentID)
|
||||
}
|
||||
|
||||
if h.aiHandler != nil && forgeCfg.AIEnabled {
|
||||
@@ -461,6 +502,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
Platform: auth.Platform,
|
||||
Arch: auth.Arch,
|
||||
OSVersion: auth.OSVersion,
|
||||
Hostname: auth.Hostname,
|
||||
Capabilities: &caps,
|
||||
}
|
||||
|
||||
@@ -496,14 +538,22 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
oldConn.Close()
|
||||
h.mu.Lock()
|
||||
}
|
||||
h.agents[agentID] = &AgentConnection{AgentID: agentID, Conn: conn}
|
||||
ac := &AgentConnection{AgentID: agentID, Conn: conn}
|
||||
h.agents[agentID] = ac
|
||||
h.mu.Unlock()
|
||||
|
||||
// Start the RTT-aware ping loop now that we have an AgentConnection.
|
||||
go h.runPingLoopAgent(ac)
|
||||
|
||||
conn.WriteJSON(Message{Type: "auth_response", Payload: mustMarshal(map[string]interface{}{
|
||||
"success": true,
|
||||
"agent_id": agentID,
|
||||
})})
|
||||
|
||||
// Enrich agent with hostname before broadcasting so the dashboard
|
||||
// immediately shows the correct machine-specific display name.
|
||||
agent.Hostname = auth.Hostname
|
||||
|
||||
h.broadcastDashboard(Message{
|
||||
Type: "agent_online",
|
||||
Payload: mustMarshal(agent),
|
||||
@@ -659,6 +709,14 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
if len(stats.Services) > 0 {
|
||||
broadcast["services"] = stats.Services
|
||||
}
|
||||
// Attach latest RTT latency from the ping loop.
|
||||
if ac := h.getAgentConn(agentID); ac != nil {
|
||||
ac.latencyMu.Lock()
|
||||
if ac.LatencyMs != nil {
|
||||
broadcast["latency_ms"] = *ac.LatencyMs
|
||||
}
|
||||
ac.latencyMu.Unlock()
|
||||
}
|
||||
h.broadcastDashboard(Message{Type: "stats_update", Payload: mustMarshal(broadcast)})
|
||||
|
||||
case "submit_share":
|
||||
@@ -751,11 +809,18 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
var proxy *pool.Proxy
|
||||
if h.poolManager != nil {
|
||||
poolCfg := h.agentPoolConfig(agentID)
|
||||
// Only use GetPool (non-blocking). If the pool hasn't connected yet
|
||||
// (background EnsurePoolWithBackups from auth is still dialing), kick
|
||||
// off another async attempt rather than blocking the WS read loop.
|
||||
proxy = h.poolManager.GetPool(&poolCfg)
|
||||
if proxy == nil {
|
||||
if p, err := h.poolManager.EnsurePool(&poolCfg); err == nil {
|
||||
proxy = p
|
||||
}
|
||||
go func(pc pool.Config) {
|
||||
if p, err := h.poolManager.EnsurePool(&pc); err != nil {
|
||||
log.Printf("[WS] get_job EnsurePool for %s failed: %v", pc.Host, err)
|
||||
} else {
|
||||
_ = p
|
||||
}
|
||||
}(poolCfg)
|
||||
}
|
||||
}
|
||||
if proxy != nil {
|
||||
@@ -763,10 +828,10 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
if job != nil {
|
||||
conn.WriteJSON(Message{Type: "new_job", Payload: mustMarshal(job)})
|
||||
} else {
|
||||
conn.WriteJSON(Message{Type: "new_job", Payload: mustMarshal(map[string]string{"error": "no job available"})})
|
||||
conn.WriteJSON(Message{Type: "new_job", Payload: mustMarshal(map[string]string{"error": "no job available — pool connecting"})})
|
||||
}
|
||||
} else {
|
||||
conn.WriteJSON(Message{Type: "new_job", Payload: mustMarshal(map[string]string{"error": "pool not connected"})})
|
||||
conn.WriteJSON(Message{Type: "new_job", Payload: mustMarshal(map[string]string{"error": "pool connecting — retry shortly"})})
|
||||
}
|
||||
|
||||
case "log_tail":
|
||||
@@ -920,6 +985,29 @@ func (h *WSHub) SendToAgent(agentID string, msg Message) error {
|
||||
return agent.SendJSON(msg)
|
||||
}
|
||||
|
||||
// RemoveAgent forcibly disconnects an agent and removes it from the live map.
|
||||
// It then broadcasts agent_deleted to all dashboard clients so the UI removes
|
||||
// the agent immediately without waiting for the disconnect goroutine to fire.
|
||||
func (h *WSHub) RemoveAgent(agentID string) {
|
||||
h.mu.Lock()
|
||||
if ac, ok := h.agents[agentID]; ok {
|
||||
// Nil the map entry BEFORE closing so the agent goroutine's deferred
|
||||
// cleanup (which checks cur.Conn == conn) falls into the else branch
|
||||
// and skips SetAgentOffline — avoiding a write to an already-deleted row.
|
||||
delete(h.agents, agentID)
|
||||
delete(h.agentConfigs, agentID)
|
||||
delete(h.agentLogs, agentID)
|
||||
delete(h.agentCapabilities, agentID)
|
||||
ac.Conn.Close()
|
||||
}
|
||||
h.mu.Unlock()
|
||||
// Broadcast deletion so every connected dashboard removes the agent immediately.
|
||||
h.broadcastDashboard(Message{
|
||||
Type: "agent_deleted",
|
||||
Payload: mustMarshal(map[string]string{"agent_id": agentID}),
|
||||
})
|
||||
}
|
||||
|
||||
// SendAgentCommand sends a remote command to an agent.
|
||||
func (h *WSHub) SendAgentCommand(agentID, action string, args map[string]interface{}) error {
|
||||
payload := map[string]interface{}{"action": action}
|
||||
@@ -970,17 +1058,23 @@ func (h *WSHub) BroadcastAIActivity(entry interface{}) {
|
||||
h.broadcastDashboard(Message{Type: "ai_activity", Payload: mustMarshal(entry)})
|
||||
}
|
||||
|
||||
// agentDisplayName returns a display name that is unique per physical machine.
|
||||
// Hostname is preferred because it's machine-specific — many agents deployed from
|
||||
// the same binary would otherwise share the same baked-in worker name, making
|
||||
// a large fleet impossible to differentiate.
|
||||
func agentDisplayName(workerName, worker, hostname, agentID string) string {
|
||||
if workerName != "" {
|
||||
return workerName
|
||||
}
|
||||
if worker != "" {
|
||||
return worker
|
||||
}
|
||||
if hostname != "" {
|
||||
return hostname
|
||||
}
|
||||
return shortAgentID(agentID)
|
||||
// No hostname reported — make the worker name unique with a short agent ID suffix.
|
||||
base := workerName
|
||||
if base == "" {
|
||||
base = worker
|
||||
}
|
||||
if base == "" {
|
||||
base = "agent"
|
||||
}
|
||||
return base + "-" + shortAgentID(agentID)
|
||||
}
|
||||
|
||||
func shortAgentID(id string) string {
|
||||
|
||||
@@ -450,8 +450,11 @@ func (h *Handler) DownloadUninstall(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Build not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
uninstallPath := strings.TrimSuffix(build.FilePath, filepath.Base(build.FilePath)) +
|
||||
fmt.Sprintf("uninstall-%s.ps1", sanitizeFileName(build.WorkerName))
|
||||
// Uninstall script lives in buildDir (builds/<id>/), NOT in the platform
|
||||
// sub-directory where the binary lives (builds/<id>/windows-amd64/).
|
||||
// Compute directly from dataDir + buildID to avoid path-stripping mistakes.
|
||||
buildDir := filepath.Join(h.dataDir, "builds", buildID)
|
||||
uninstallPath := filepath.Join(buildDir, fmt.Sprintf("uninstall-%s.ps1", sanitizeFileName(build.WorkerName)))
|
||||
if _, err := os.Stat(uninstallPath); err != nil {
|
||||
http.Error(w, "Uninstall script missing", http.StatusNotFound)
|
||||
return
|
||||
@@ -1069,7 +1072,7 @@ func formatGoBackupPools(pools []BackupPool) string {
|
||||
return "nil"
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.WriteString("[]config.BackupPool{")
|
||||
sb.WriteString("[]BackupPool{")
|
||||
for i, p := range pools {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
|
||||
@@ -76,6 +76,15 @@ func generateUninstallScript(buildID string, req *BuildRequest) string {
|
||||
installRel := expandInstallRelativePath(req, buildID)
|
||||
installBase := resolveInstallBasePS(req)
|
||||
|
||||
firewallBool := "$false"
|
||||
if req.FirewallExclusion {
|
||||
firewallBool = "$true"
|
||||
}
|
||||
pauseBool := "$false"
|
||||
if !req.StealthMode {
|
||||
pauseBool = "$true"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`# AetherForge Miner Uninstaller
|
||||
# Worker: %s
|
||||
# Generated alongside forged installer — run as the same Windows user who installed the miner.
|
||||
@@ -112,25 +121,22 @@ if (Test-Path $ExpectedExe) {
|
||||
|
||||
Write-Host "Removing persistence..."
|
||||
Remove-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -Name $PersistenceKey -ErrorAction SilentlyContinue
|
||||
Unregister-ScheduledTask -TaskName $PersistenceKey -Confirm:$false -ErrorAction SilentlyContinue
|
||||
|
||||
if (%t) {
|
||||
if (%s) {
|
||||
Write-Host "Removing Windows Firewall rules..."
|
||||
Remove-NetFirewallRule -DisplayName ('AetherForge ' + $PersistenceKey + ' In') -ErrorAction SilentlyContinue
|
||||
Remove-NetFirewallRule -DisplayName ('AetherForge ' + $PersistenceKey + ' Out') -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if ($true) {
|
||||
Unregister-ScheduledTask -TaskName $PersistenceKey -Confirm:$false -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Host "Removing install directory: $InstallDir"
|
||||
if ($InstallDir -and (Test-Path $InstallDir)) {
|
||||
Remove-Item -LiteralPath $InstallDir -Recurse -Force
|
||||
}
|
||||
|
||||
Write-Host "Done. Miner removed."
|
||||
if (%t) { Read-Host 'Press Enter to close' }
|
||||
`, req.WorkerName, processName, persistenceKey, installBase, strings.ReplaceAll(installRel, "'", "''"), req.FirewallExclusion, !req.StealthMode)
|
||||
if (%s) { Read-Host 'Press Enter to close' }
|
||||
`, req.WorkerName, processName, persistenceKey, installBase, strings.ReplaceAll(installRel, "'", "''"), firewallBool, pauseBool)
|
||||
}
|
||||
|
||||
func (h *Handler) writeUninstallScript(buildDir string, buildID string, req *BuildRequest) (fileName, filePath string, err error) {
|
||||
|
||||
@@ -40,7 +40,7 @@ func (d *Database) scanAgent(row interface {
|
||||
&a.Hashrate15s, &a.Hashrate1m, &a.Hashrate15m,
|
||||
&a.SharesTotal, &a.SharesGood, &a.SharesBad,
|
||||
&a.CPUUsagePct, &a.MemoryUsagePct, &a.UptimeSeconds,
|
||||
¬es, &tagsRaw, &a.Platform, &a.Arch, &a.OSVersion,
|
||||
¬es, &tagsRaw, &a.Platform, &a.Arch, &a.OSVersion, &a.Hostname,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -52,7 +52,7 @@ func (d *Database) scanAgent(row interface {
|
||||
|
||||
const agentSelectCols = `id, name, wallet, ip, version, status, cpu_cores, memory_gb, last_seen, created_at,
|
||||
hashrate_15s, hashrate_1m, hashrate_15m, shares_total, shares_good, shares_bad,
|
||||
cpu_usage_pct, memory_usage_pct, uptime_seconds, notes, tags, platform, arch, os_version`
|
||||
cpu_usage_pct, memory_usage_pct, uptime_seconds, notes, tags, platform, arch, os_version, hostname`
|
||||
|
||||
func (d *Database) UpdateAgentMeta(id, notes string, tags []string) error {
|
||||
_, err := d.Exec(`UPDATE agents SET notes = ?, tags = ? WHERE id = ?`, notes, encodeTags(tags), id)
|
||||
|
||||
@@ -124,6 +124,7 @@ func (d *Database) migrate() error {
|
||||
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN platform TEXT NOT NULL DEFAULT ''`)
|
||||
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN arch TEXT NOT NULL DEFAULT ''`)
|
||||
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN os_version TEXT NOT NULL DEFAULT ''`)
|
||||
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN hostname TEXT NOT NULL DEFAULT ''`)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -131,8 +132,8 @@ func (d *Database) migrate() error {
|
||||
// Agent operations
|
||||
|
||||
func (d *Database) UpsertAgent(a *models.Agent) error {
|
||||
query := `INSERT INTO agents (id, name, wallet, ip, version, status, cpu_cores, memory_gb, last_seen, created_at, platform, arch, os_version)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM agents WHERE id = ?), CURRENT_TIMESTAMP), ?, ?, ?)
|
||||
query := `INSERT INTO agents (id, name, wallet, ip, version, status, cpu_cores, memory_gb, last_seen, created_at, platform, arch, os_version, hostname)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM agents WHERE id = ?), CURRENT_TIMESTAMP), ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
wallet = excluded.wallet,
|
||||
@@ -144,8 +145,9 @@ func (d *Database) UpsertAgent(a *models.Agent) error {
|
||||
last_seen = excluded.last_seen,
|
||||
platform = excluded.platform,
|
||||
arch = excluded.arch,
|
||||
os_version = excluded.os_version`
|
||||
_, err := d.Exec(query, a.ID, a.Name, a.Wallet, a.IP, a.Version, a.Status, a.CPUCores, a.MemoryGB, a.LastSeen, a.ID, a.Platform, a.Arch, a.OSVersion)
|
||||
os_version = excluded.os_version,
|
||||
hostname = excluded.hostname`
|
||||
_, err := d.Exec(query, a.ID, a.Name, a.Wallet, a.IP, a.Version, a.Status, a.CPUCores, a.MemoryGB, a.LastSeen, a.ID, a.Platform, a.Arch, a.OSVersion, a.Hostname)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -165,6 +167,11 @@ func (d *Database) SetAgentOffline(id string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) DeleteAgent(id string) error {
|
||||
_, err := d.Exec("DELETE FROM agents WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) GetAgent(id string) (*models.Agent, error) {
|
||||
query := `SELECT ` + agentSelectCols + ` FROM agents WHERE id = ?`
|
||||
return d.scanAgent(d.QueryRow(query, id))
|
||||
|
||||
@@ -31,6 +31,10 @@ type Agent struct {
|
||||
Platform string `json:"platform,omitempty"`
|
||||
Arch string `json:"arch,omitempty"`
|
||||
OSVersion string `json:"os_version,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
|
||||
// Live connection quality — not persisted, set by WSHub each stats cycle.
|
||||
LatencyMs *int `json:"latency_ms,omitempty"`
|
||||
|
||||
Capabilities *AgentCapabilities `json:"capabilities,omitempty"`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user