package mining import ( "encoding/json" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "strings" "time" "github.com/Snider/Mining/pkg/logging" ) // Start launches the XMRig miner with the specified configuration. func (m *XMRigMiner) Start(config *Config) error { // Check installation BEFORE acquiring lock (CheckInstallation takes its own locks) m.mu.RLock() needsInstallCheck := m.MinerBinary == "" m.mu.RUnlock() if needsInstallCheck { if _, err := m.CheckInstallation(); err != nil { return err // Propagate the detailed error from CheckInstallation } } m.mu.Lock() defer m.mu.Unlock() if m.Running { return errors.New("miner is already running") } if m.API != nil && config.HTTPPort != 0 { m.API.ListenPort = config.HTTPPort } else if m.API != nil && m.API.ListenPort == 0 { return errors.New("miner API port not assigned") } if config.Pool != "" && config.Wallet != "" { if err := m.createConfig(config); err != nil { return err } } else { // Use the centralized helper to get the instance-specific config path configPath, err := getXMRigConfigPath(m.Name) if err != nil { return fmt.Errorf("could not determine config file path: %w", err) } m.ConfigPath = configPath if _, err := os.Stat(m.ConfigPath); os.IsNotExist(err) { return errors.New("config file does not exist and no pool/wallet provided to create one") } } args := []string{"-c", m.ConfigPath} if m.API != nil && m.API.Enabled { args = append(args, "--http-host", m.API.ListenHost, "--http-port", fmt.Sprintf("%d", m.API.ListenPort)) } addCliArgs(config, &args) logging.Info("executing miner command", logging.Fields{"binary": m.MinerBinary, "args": strings.Join(args, " ")}) m.cmd = exec.Command(m.MinerBinary, args...) // Create stdin pipe for console commands stdinPipe, err := m.cmd.StdinPipe() if err != nil { return fmt.Errorf("failed to create stdin pipe: %w", err) } m.stdinPipe = stdinPipe // Always capture output to LogBuffer if m.LogBuffer != nil { m.cmd.Stdout = m.LogBuffer m.cmd.Stderr = m.LogBuffer } // Also output to console if requested if config.LogOutput { m.cmd.Stdout = io.MultiWriter(m.LogBuffer, os.Stdout) m.cmd.Stderr = io.MultiWriter(m.LogBuffer, os.Stderr) } if err := m.cmd.Start(); err != nil { stdinPipe.Close() // Clean up config file on failed start if m.ConfigPath != "" { os.Remove(m.ConfigPath) } return fmt.Errorf("failed to start miner: %w", err) } m.Running = true // Capture cmd locally to avoid race with Stop() cmd := m.cmd minerName := m.Name // Capture name for logging go func() { // Use a channel to detect if Wait() completes done := make(chan struct{}) var waitErr error go func() { waitErr = cmd.Wait() close(done) }() // Wait with timeout to prevent goroutine leak on zombie processes select { case <-done: // Normal exit - log the exit status if waitErr != nil { logging.Info("miner process exited", logging.Fields{ "miner": minerName, "error": waitErr.Error(), }) } else { logging.Info("miner process exited normally", logging.Fields{ "miner": minerName, }) } case <-time.After(5 * time.Minute): // Process didn't exit after 5 minutes - force cleanup logging.Warn("miner process wait timeout, forcing cleanup", logging.Fields{"miner": minerName}) if cmd.Process != nil { cmd.Process.Kill() } // Wait with timeout to prevent goroutine leak if Wait() never returns select { case <-done: // Inner goroutine completed case <-time.After(10 * time.Second): logging.Error("process cleanup timed out after kill", logging.Fields{"miner": minerName}) } } m.mu.Lock() // Only clear if this is still the same command (not restarted) if m.cmd == cmd { m.Running = false m.cmd = nil } m.mu.Unlock() }() return nil } // Stop terminates the miner process and cleans up the instance-specific config file. func (m *XMRigMiner) Stop() error { // Call the base Stop to kill the process if err := m.BaseMiner.Stop(); err != nil { return err } // Clean up the instance-specific config file if m.ConfigPath != "" { os.Remove(m.ConfigPath) // Ignore error if it doesn't exist } return nil } // addCliArgs is a helper to append command line arguments based on the config. func addCliArgs(config *Config, args *[]string) { if config.Pool != "" { *args = append(*args, "-o", config.Pool) } if config.Wallet != "" { *args = append(*args, "-u", config.Wallet) } if config.Threads != 0 { *args = append(*args, "-t", fmt.Sprintf("%d", config.Threads)) } if !config.HugePages { *args = append(*args, "--no-huge-pages") } if config.TLS { *args = append(*args, "--tls") } *args = append(*args, "--donate-level", "1") } // createConfig creates a JSON configuration file for the XMRig miner. func (m *XMRigMiner) createConfig(config *Config) error { // Use the centralized helper to get the instance-specific config path configPath, err := getXMRigConfigPath(m.Name) if err != nil { return err } m.ConfigPath = configPath if err := os.MkdirAll(filepath.Dir(m.ConfigPath), 0755); err != nil { return err } apiListen := "127.0.0.1:0" if m.API != nil { apiListen = fmt.Sprintf("%s:%d", m.API.ListenHost, m.API.ListenPort) } cpuConfig := map[string]interface{}{ "enabled": true, "huge-pages": config.HugePages, } // Set thread count or max-threads-hint for CPU throttling if config.Threads > 0 { cpuConfig["threads"] = config.Threads } if config.CPUMaxThreadsHint > 0 { cpuConfig["max-threads-hint"] = config.CPUMaxThreadsHint } if config.CPUPriority > 0 { cpuConfig["priority"] = config.CPUPriority } // Build pools array - CPU pool first cpuPool := map[string]interface{}{ "url": config.Pool, "user": config.Wallet, "pass": "x", "keepalive": true, "tls": config.TLS, } // Add algo or coin (coin takes precedence for algorithm auto-detection) if config.Coin != "" { cpuPool["coin"] = config.Coin } else if config.Algo != "" { cpuPool["algo"] = config.Algo } pools := []map[string]interface{}{cpuPool} // Add separate GPU pool if configured if config.GPUEnabled && config.GPUPool != "" { gpuWallet := config.GPUWallet if gpuWallet == "" { gpuWallet = config.Wallet // Default to main wallet } gpuPass := config.GPUPassword if gpuPass == "" { gpuPass = "x" } gpuPool := map[string]interface{}{ "url": config.GPUPool, "user": gpuWallet, "pass": gpuPass, "keepalive": true, } // Add GPU algo (typically etchash, ethash, kawpow, progpowz for GPU mining) if config.GPUAlgo != "" { gpuPool["algo"] = config.GPUAlgo } pools = append(pools, gpuPool) } // Build OpenCL (AMD/Intel GPU) config // GPU mining requires explicit device selection - no auto-picking openclConfig := map[string]interface{}{ "enabled": config.GPUEnabled && config.OpenCL && config.Devices != "", } if config.GPUEnabled && config.OpenCL && config.Devices != "" { // User must explicitly specify devices (e.g., "0" or "0,1") openclConfig["devices"] = config.Devices if config.GPUIntensity > 0 { openclConfig["intensity"] = config.GPUIntensity } if config.GPUThreads > 0 { openclConfig["threads"] = config.GPUThreads } } // Build CUDA (NVIDIA GPU) config // GPU mining requires explicit device selection - no auto-picking cudaConfig := map[string]interface{}{ "enabled": config.GPUEnabled && config.CUDA && config.Devices != "", } if config.GPUEnabled && config.CUDA && config.Devices != "" { // User must explicitly specify devices (e.g., "0" or "0,1") cudaConfig["devices"] = config.Devices if config.GPUIntensity > 0 { cudaConfig["intensity"] = config.GPUIntensity } if config.GPUThreads > 0 { cudaConfig["threads"] = config.GPUThreads } } c := map[string]interface{}{ "api": map[string]interface{}{ "enabled": m.API != nil && m.API.Enabled, "listen": apiListen, "restricted": true, }, "pools": pools, "cpu": cpuConfig, "opencl": openclConfig, "cuda": cudaConfig, "pause-on-active": config.PauseOnActive, "pause-on-battery": config.PauseOnBattery, } data, err := json.MarshalIndent(c, "", " ") if err != nil { return err } return os.WriteFile(m.ConfigPath, data, 0600) }