Implement Windows agent with RandomX mining and WebSocket fleet reporting, wire dashboard settings into the builder with saved exe paths, and add project README.
264 lines
7.1 KiB
Go
264 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"crypto-miner-server/internal/api"
|
|
"crypto-miner-server/internal/builder"
|
|
"crypto-miner-server/internal/db"
|
|
"crypto-miner-server/internal/pool"
|
|
)
|
|
|
|
func main() {
|
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
|
log.Println("Crypto Miner Control Server starting...")
|
|
|
|
// Load configuration
|
|
cfg := LoadConfig()
|
|
log.Printf("Configuration loaded: port=%d, dataDir=%s", cfg.Port, cfg.DataDir)
|
|
|
|
// Ensure data directories exist
|
|
dirs := []string{
|
|
cfg.DataDir,
|
|
filepath.Join(cfg.DataDir, "builds"),
|
|
filepath.Join(cfg.DataDir, "logs"),
|
|
}
|
|
for _, dir := range dirs {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
log.Fatalf("Failed to create directory %s: %v", dir, err)
|
|
}
|
|
}
|
|
|
|
// Initialize database
|
|
database, err := db.New(cfg.DataDir)
|
|
if err != nil {
|
|
log.Fatalf("Failed to initialize database: %v", err)
|
|
}
|
|
defer database.Close()
|
|
log.Println("Database initialized")
|
|
|
|
// Initialize WebSocket hub
|
|
wsHub := api.NewWSHub(database)
|
|
wsHub.SetDefaultAgentConfig(cfg.DefaultAgent)
|
|
log.Println("WebSocket hub initialized")
|
|
|
|
// Initialize config provider (wraps the config for the API handler)
|
|
configProvider := &serverConfigProvider{config: cfg}
|
|
|
|
// Initialize config handler
|
|
configHandler := api.NewConfigHandler(database, configProvider)
|
|
log.Println("Config handler initialized")
|
|
|
|
// Initialize builder handler
|
|
// The agent source is expected at ../agent relative to the server directory
|
|
agentSrcDir := findAgentSourceDir()
|
|
projectRoot := findProjectRoot()
|
|
builderHandler := builder.NewHandler(database, cfg.DataDir, agentSrcDir, projectRoot)
|
|
log.Printf("Builder handler initialized (agent source: %s)", agentSrcDir)
|
|
|
|
// Initialize Stratum pool proxy
|
|
poolCfg := &pool.Config{
|
|
Host: cfg.Pool.Host,
|
|
Port: cfg.Pool.Port,
|
|
UseTLS: cfg.Pool.UseTLS,
|
|
Wallet: cfg.Wallet.Address,
|
|
Password: cfg.Pool.Password,
|
|
}
|
|
poolProxy := pool.NewProxy(poolCfg)
|
|
|
|
// Set pool proxy on WebSocket hub for share forwarding
|
|
wsHub.SetPoolProxy(poolProxy)
|
|
|
|
// Set up pool callbacks
|
|
poolProxy.SetCallbacks(
|
|
// onJob - new job from pool, broadcast to all agents
|
|
func(job *pool.Job) {
|
|
log.Printf("[Pool] New job received: ID=%s, Height=%d", job.ID, job.Height)
|
|
// Broadcast new job to all connected agents
|
|
payload, _ := json.Marshal(job)
|
|
wsHub.BroadcastToAgents(api.Message{
|
|
Type: "new_job",
|
|
Payload: payload,
|
|
})
|
|
},
|
|
// onShare - share submission result from pool
|
|
func(accepted bool, agentID string, jobID string) {
|
|
log.Printf("[Pool] Share result for agent %s (job: %s): accepted=%v", agentID, jobID, accepted)
|
|
},
|
|
// onError - pool connection error
|
|
func(err error) {
|
|
log.Printf("[Pool] Error: %v", err)
|
|
},
|
|
)
|
|
|
|
// Start pool proxy connection (non-blocking, runs in background)
|
|
go func() {
|
|
if err := poolProxy.Start(); err != nil {
|
|
log.Printf("[Pool] Failed to connect to pool (will retry): %v", err)
|
|
}
|
|
}()
|
|
|
|
// Find web root for frontend
|
|
webRoot := findWebRoot()
|
|
log.Printf("Web root: %s", webRoot)
|
|
|
|
// Initialize router
|
|
router := api.NewRouter(database, wsHub, configHandler, builderHandler, webRoot)
|
|
log.Println("Router initialized")
|
|
|
|
// Start server
|
|
addr := fmt.Sprintf(":%d", cfg.Port)
|
|
log.Printf("Server listening on %s", addr)
|
|
log.Printf("Open http://localhost:%d in your browser", cfg.Port)
|
|
|
|
if err := http.ListenAndServe(addr, router); err != nil {
|
|
log.Fatalf("Server failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// serverConfigProvider wraps the Config to implement api.ConfigProvider interface
|
|
type serverConfigProvider struct {
|
|
config *Config
|
|
}
|
|
|
|
func (p *serverConfigProvider) GetConfigJSON() json.RawMessage {
|
|
data, _ := json.Marshal(p.config)
|
|
return data
|
|
}
|
|
|
|
func (p *serverConfigProvider) UpdateConfigFromJSON(data json.RawMessage) error {
|
|
var incoming Config
|
|
if err := json.Unmarshal(data, &incoming); err != nil {
|
|
return fmt.Errorf("invalid config: %w", err)
|
|
}
|
|
|
|
// Merge incoming config over current config
|
|
mergeConfig(p.config, &incoming)
|
|
|
|
// Save to disk
|
|
if err := p.config.Save(); err != nil {
|
|
return fmt.Errorf("failed to save config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// findAgentSourceDir locates the agent source code directory
|
|
// It searches relative to the server binary location and the current working directory
|
|
func findAgentSourceDir() string {
|
|
projectRoot := findProjectRoot()
|
|
candidates := []string{
|
|
filepath.Join(projectRoot, "agent"),
|
|
"../agent",
|
|
"./agent",
|
|
}
|
|
|
|
if cwd, err := os.Getwd(); err == nil {
|
|
candidates = append(candidates,
|
|
filepath.Join(cwd, "agent"),
|
|
filepath.Join(filepath.Dir(cwd), "agent"),
|
|
)
|
|
}
|
|
|
|
if exe, err := os.Executable(); err == nil {
|
|
exeDir := filepath.Dir(exe)
|
|
candidates = append(candidates,
|
|
filepath.Join(exeDir, "agent"),
|
|
filepath.Join(exeDir, "..", "agent"),
|
|
filepath.Join(exeDir, "..", "..", "agent"),
|
|
)
|
|
}
|
|
|
|
seen := map[string]bool{}
|
|
for _, candidate := range candidates {
|
|
absPath, err := filepath.Abs(candidate)
|
|
if err != nil || seen[absPath] {
|
|
continue
|
|
}
|
|
seen[absPath] = true
|
|
goModPath := filepath.Join(absPath, "go.mod")
|
|
if _, err := os.Stat(goModPath); err == nil {
|
|
return absPath
|
|
}
|
|
}
|
|
|
|
return filepath.Join(projectRoot, "agent")
|
|
}
|
|
|
|
func findProjectRoot() string {
|
|
if cwd, err := os.Getwd(); err == nil {
|
|
if _, err := os.Stat(filepath.Join(cwd, "run.bat")); err == nil {
|
|
return cwd
|
|
}
|
|
if _, err := os.Stat(filepath.Join(filepath.Dir(cwd), "run.bat")); err == nil {
|
|
return filepath.Dir(cwd)
|
|
}
|
|
}
|
|
if exe, err := os.Executable(); err == nil {
|
|
exeDir := filepath.Dir(exe)
|
|
candidates := []string{
|
|
exeDir,
|
|
filepath.Join(exeDir, ".."),
|
|
filepath.Join(exeDir, "..", ".."),
|
|
}
|
|
for _, candidate := range candidates {
|
|
if _, err := os.Stat(filepath.Join(candidate, "run.bat")); err == nil {
|
|
abs, _ := filepath.Abs(candidate)
|
|
return abs
|
|
}
|
|
}
|
|
}
|
|
if cwd, err := os.Getwd(); err == nil {
|
|
return cwd
|
|
}
|
|
return "."
|
|
}
|
|
|
|
// findWebRoot locates the frontend build output directory
|
|
func findWebRoot() string {
|
|
candidates := []string{
|
|
"webroot", // Copied by run.bat
|
|
"web/dist", // Vite build output relative to server/
|
|
filepath.Join("..", "server", "web", "dist"), // Relative to project root
|
|
filepath.Join("server", "web", "dist"), // From project root
|
|
}
|
|
|
|
if cwd, err := os.Getwd(); err == nil {
|
|
candidates = append(candidates,
|
|
filepath.Join(cwd, "webroot"),
|
|
filepath.Join(cwd, "web", "dist"),
|
|
filepath.Join(filepath.Dir(cwd), "server", "webroot"),
|
|
filepath.Join(filepath.Dir(cwd), "server", "web", "dist"),
|
|
)
|
|
}
|
|
|
|
if exe, err := os.Executable(); err == nil {
|
|
exeDir := filepath.Dir(exe)
|
|
candidates = append(candidates,
|
|
filepath.Join(exeDir, "..", "webroot"),
|
|
filepath.Join(exeDir, "..", "web", "dist"),
|
|
filepath.Join(exeDir, "..", "..", "server", "webroot"),
|
|
filepath.Join(exeDir, "..", "..", "server", "web", "dist"),
|
|
)
|
|
}
|
|
|
|
for _, candidate := range candidates {
|
|
absPath, err := filepath.Abs(candidate)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
// Check if it has index.html
|
|
indexPath := filepath.Join(absPath, "index.html")
|
|
if _, err := os.Stat(indexPath); err == nil {
|
|
return absPath
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|