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 "" }