Complete private Monero miner control stack.

Implement Windows agent with RandomX mining and WebSocket fleet reporting, wire dashboard settings into the builder with saved exe paths, and add project README.
This commit is contained in:
drjones
2026-05-26 22:51:47 -07:00
commit 6c42f2b600
48 changed files with 10001 additions and 0 deletions

263
server/main.go Normal file
View File

@@ -0,0 +1,263 @@
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 ""
}