Mining/cmd/mining/cmd/simulate.go
Virgil 9102b25f55
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run
Apply AX naming and comment cleanup
2026-04-04 04:16:13 +00:00

187 lines
6 KiB
Go

package cmd
import (
"context"
"fmt"
"math/rand"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"forge.lthn.ai/Snider/Mining/pkg/mining"
"github.com/spf13/cobra"
)
var (
simCount int
simPreset string
simHashrate int
simAlgorithm string
)
// simulateCmd.Use == "simulate" and RunE starts the service with fake miners.
var simulateCmd = &cobra.Command{
Use: "simulate",
Short: "Start the service with simulated miners for UI testing",
Long: `Start the mining service with simulated miners that generate realistic
hashrate data and statistics. This is useful for UI development and testing
without requiring actual mining hardware.
Examples:
# Start with 3 medium-hashrate CPU miners
mining simulate --count 3 --preset cpu-medium
# Start with custom hashrate
mining simulate --count 2 --hashrate 8000 --algorithm rx/0
# Start with a mix of presets
mining simulate --count 1 --preset gpu-ethash
Available presets:
cpu-low - Low-end CPU (500 H/s, rx/0)
cpu-medium - Medium CPU (5 kH/s, rx/0)
cpu-high - High-end CPU (15 kH/s, rx/0)
gpu-ethash - GPU mining ETH (30 MH/s, ethash)
gpu-kawpow - GPU mining RVN (15 MH/s, kawpow)`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
displayHost := host
if displayHost == "0.0.0.0" {
var err error
displayHost, err = getLocalIP()
if err != nil {
displayHost = "localhost"
}
}
displayAddr := fmt.Sprintf("%s:%d", displayHost, port)
listenAddr := fmt.Sprintf("%s:%d", host, port)
// Create a dedicated simulation manager without autostarting real miners.
manager := mining.NewManagerForSimulation()
// getSimulatedConfig(i) // derives one miner profile per index, e.g. sim-cpu-medium-001
for i := 0; i < simCount; i++ {
config := getSimulatedConfig(i)
simMiner := mining.NewSimulatedMiner(config)
// simMiner.Start(&mining.Config{}) // uses the simulated miner lifecycle, not a real binary
if err := simMiner.Start(&mining.Config{}); err != nil {
return fmt.Errorf("failed to start simulated miner %d: %w", i, err)
}
// manager.RegisterMiner(simMiner) // exposes the simulated miner through the shared manager
if err := manager.RegisterMiner(simMiner); err != nil {
return fmt.Errorf("failed to register simulated miner %d: %w", i, err)
}
fmt.Printf("Started simulated miner: %s (%s, ~%d H/s)\n",
config.Name, config.Algorithm, config.BaseHashrate)
}
// Create and start the service
service, err := mining.NewService(manager, listenAddr, displayAddr, namespace)
if err != nil {
return fmt.Errorf("failed to create new service: %w", err)
}
// service.ServiceStartup(ctx) // starts the API server while the simulation loop keeps running.
go func() {
if err := service.ServiceStartup(ctx); err != nil {
fmt.Fprintf(os.Stderr, "Failed to start service: %v\n", err)
cancel()
}
}()
fmt.Printf("\n=== SIMULATION MODE ===\n")
fmt.Printf("Mining service started on http://%s:%d\n", displayHost, port)
fmt.Printf("Swagger documentation is available at http://%s:%d%s/swagger/index.html\n", displayHost, port, namespace)
fmt.Printf("\nSimulating %d miner(s). Press Ctrl+C to stop.\n", simCount)
fmt.Printf("Note: All data is simulated - no actual mining is occurring.\n\n")
// signalChannel captures Ctrl+C so simulated miners can stop cleanly.
signalChannel := make(chan os.Signal, 1)
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM)
select {
case <-signalChannel:
fmt.Println("\nReceived shutdown signal, stopping simulation...")
cancel()
case <-ctx.Done():
}
// Stop all simulated miners
for _, miner := range manager.ListMiners() {
manager.StopMiner(context.Background(), miner.GetName())
}
fmt.Println("Simulation stopped.")
return nil
},
}
// getSimulatedConfig returns configuration for a simulated miner based on flags.
func getSimulatedConfig(index int) mining.SimulatedMinerConfig {
// Generate unique name
name := fmt.Sprintf("sim-%s-%03d", simPreset, index+1)
// Start with preset if specified
var config mining.SimulatedMinerConfig
if preset, ok := mining.SimulatedMinerPresets[simPreset]; ok {
config = preset
} else {
// Default preset
config = mining.SimulatedMinerPresets["cpu-medium"]
}
config.Name = name
// Override with custom values if provided
if simHashrate > 0 {
config.BaseHashrate = simHashrate
}
if simAlgorithm != "" {
config.Algorithm = simAlgorithm
}
// Add some variance between miners
variance := 0.1 + rand.Float64()*0.1 // 10-20% variance
config.BaseHashrate = int(float64(config.BaseHashrate) * (0.9 + rand.Float64()*0.2))
config.Variance = variance
return config
}
func init() {
// rand.Seed(time.Now().UnixNano()) // varies simulated hash rates between runs
rand.Seed(time.Now().UnixNano())
simulateCmd.Flags().IntVarP(&simCount, "count", "c", 1, "Number of simulated miners to create")
simulateCmd.Flags().StringVar(&simPreset, "preset", "cpu-medium", "Miner preset (cpu-low, cpu-medium, cpu-high, gpu-ethash, gpu-kawpow)")
simulateCmd.Flags().IntVar(&simHashrate, "hashrate", 0, "Custom base hashrate (overrides preset)")
simulateCmd.Flags().StringVar(&simAlgorithm, "algorithm", "", "Custom algorithm (overrides preset)")
// simulateCmd.Flags().StringVar(&host, "host", "127.0.0.1", "Host to listen on") // same listen address as `mining serve`
simulateCmd.Flags().StringVar(&host, "host", "127.0.0.1", "Host to listen on")
simulateCmd.Flags().IntVarP(&port, "port", "p", 9090, "Port to listen on")
simulateCmd.Flags().StringVarP(&namespace, "namespace", "n", "/api/v1/mining", "API namespace")
rootCmd.AddCommand(simulateCmd)
}
// Helper function to format hashrate
func formatHashrate(h int) string {
if h >= 1000000000 {
return strconv.FormatFloat(float64(h)/1000000000, 'f', 2, 64) + " GH/s"
}
if h >= 1000000 {
return strconv.FormatFloat(float64(h)/1000000, 'f', 2, 64) + " MH/s"
}
if h >= 1000 {
return strconv.FormatFloat(float64(h)/1000, 'f', 2, 64) + " kH/s"
}
return strconv.Itoa(h) + " H/s"
}