From 2576d4bc1b9af8fef33aec7f3f7969967afb5dbd Mon Sep 17 00:00:00 2001 From: Snider Date: Sun, 7 Dec 2025 15:14:30 +0000 Subject: [PATCH] feat: Update server configuration and add XMRig miner management functionality --- cmd/mining/cmd/serve.go | 6 +- docs/docs.go | 15 - docs/swagger.json | 15 - docs/swagger.yaml | 14 - pkg/mining/manager.go | 131 +++--- pkg/mining/miner.go | 385 ++++++++++++++++++ pkg/mining/mining.go | 372 ++++------------- pkg/mining/ttminer.go | 53 ++- pkg/mining/xmrig.go | 820 +------------------------------------- pkg/mining/xmrig_start.go | 154 +++++++ pkg/mining/xmrig_stats.go | 49 +++ ui/src/app/app.css | 106 ++--- ui/src/app/app.html | 164 ++++++-- ui/src/app/app.ts | 237 ++++++----- 14 files changed, 1106 insertions(+), 1415 deletions(-) create mode 100644 pkg/mining/miner.go create mode 100644 pkg/mining/xmrig_start.go create mode 100644 pkg/mining/xmrig_stats.go diff --git a/cmd/mining/cmd/serve.go b/cmd/mining/cmd/serve.go index 44a8972..9a9879b 100644 --- a/cmd/mining/cmd/serve.go +++ b/cmd/mining/cmd/serve.go @@ -171,9 +171,9 @@ var serveCmd = &cobra.Command{ } func init() { - serveCmd.Flags().StringVar(&host, "host", "0.0.0.0", "Host to listen on") - serveCmd.Flags().IntVarP(&port, "port", "p", 8080, "Port to listen on") - serveCmd.Flags().StringVarP(&namespace, "namespace", "n", "/", "API namespace for the swagger UI") + serveCmd.Flags().StringVar(&host, "host", "127.0.0.1", "Host to listen on") + serveCmd.Flags().IntVarP(&port, "port", "p", 9090, "Port to listen on") + serveCmd.Flags().StringVarP(&namespace, "namespace", "n", "/api/v1/mining", "API namespace for the swagger UI") rootCmd.AddCommand(serveCmd) } diff --git a/docs/docs.go b/docs/docs.go index a75f0a6..2eda190 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -365,14 +365,12 @@ const docTemplate = `{ "type": "object", "properties": { "algo": { - "description": "Network options", "type": "string" }, "apiId": { "type": "string" }, "apiWorkerId": { - "description": "API options (can be overridden or supplemented here)", "type": "string" }, "argon2Impl": { @@ -385,7 +383,6 @@ const docTemplate = `{ "type": "integer" }, "background": { - "description": "Misc options", "type": "boolean" }, "bench": { @@ -449,7 +446,6 @@ const docTemplate = `{ "type": "string" }, "logOutput": { - "description": "New field to control stdout/stderr logging", "type": "boolean" }, "miner": { @@ -462,7 +458,6 @@ const docTemplate = `{ "type": "boolean" }, "noCpu": { - "description": "CPU backend options", "type": "boolean" }, "noDMI": { @@ -472,7 +467,6 @@ const docTemplate = `{ "type": "boolean" }, "password": { - "description": "Corresponds to -p, not --userpass", "type": "string" }, "pauseOnActive": { @@ -530,7 +524,6 @@ const docTemplate = `{ "type": "boolean" }, "syslog": { - "description": "Logging options", "type": "boolean" }, "threads": { @@ -549,7 +542,6 @@ const docTemplate = `{ "type": "string" }, "userPass": { - "description": "Corresponds to -O", "type": "string" }, "verbose": { @@ -657,31 +649,24 @@ const docTemplate = `{ "type": "string" }, "hashrateHistory": { - "description": "High-resolution (10s)", "type": "array", "items": { "$ref": "#/definitions/mining.HashratePoint" } }, - "lastHeartbeat": { - "type": "integer" - }, "lowResHashrateHistory": { - "description": "Low-resolution (1m averages)", "type": "array", "items": { "$ref": "#/definitions/mining.HashratePoint" } }, "miner_binary": { - "description": "New field for the full path to the miner executable", "type": "string" }, "name": { "type": "string" }, "path": { - "description": "This will now be the versioned folder path", "type": "string" }, "running": { diff --git a/docs/swagger.json b/docs/swagger.json index 666cedf..fd1efa3 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -359,14 +359,12 @@ "type": "object", "properties": { "algo": { - "description": "Network options", "type": "string" }, "apiId": { "type": "string" }, "apiWorkerId": { - "description": "API options (can be overridden or supplemented here)", "type": "string" }, "argon2Impl": { @@ -379,7 +377,6 @@ "type": "integer" }, "background": { - "description": "Misc options", "type": "boolean" }, "bench": { @@ -443,7 +440,6 @@ "type": "string" }, "logOutput": { - "description": "New field to control stdout/stderr logging", "type": "boolean" }, "miner": { @@ -456,7 +452,6 @@ "type": "boolean" }, "noCpu": { - "description": "CPU backend options", "type": "boolean" }, "noDMI": { @@ -466,7 +461,6 @@ "type": "boolean" }, "password": { - "description": "Corresponds to -p, not --userpass", "type": "string" }, "pauseOnActive": { @@ -524,7 +518,6 @@ "type": "boolean" }, "syslog": { - "description": "Logging options", "type": "boolean" }, "threads": { @@ -543,7 +536,6 @@ "type": "string" }, "userPass": { - "description": "Corresponds to -O", "type": "string" }, "verbose": { @@ -651,31 +643,24 @@ "type": "string" }, "hashrateHistory": { - "description": "High-resolution (10s)", "type": "array", "items": { "$ref": "#/definitions/mining.HashratePoint" } }, - "lastHeartbeat": { - "type": "integer" - }, "lowResHashrateHistory": { - "description": "Low-resolution (1m averages)", "type": "array", "items": { "$ref": "#/definitions/mining.HashratePoint" } }, "miner_binary": { - "description": "New field for the full path to the miner executable", "type": "string" }, "name": { "type": "string" }, "path": { - "description": "This will now be the versioned folder path", "type": "string" }, "running": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 04a3edf..29bd590 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -19,12 +19,10 @@ definitions: mining.Config: properties: algo: - description: Network options type: string apiId: type: string apiWorkerId: - description: API options (can be overridden or supplemented here) type: string argon2Impl: type: string @@ -33,7 +31,6 @@ definitions: av: type: integer background: - description: Misc options type: boolean bench: type: string @@ -76,7 +73,6 @@ definitions: logFile: type: string logOutput: - description: New field to control stdout/stderr logging type: boolean miner: type: string @@ -85,14 +81,12 @@ definitions: noColor: type: boolean noCpu: - description: CPU backend options type: boolean noDMI: type: boolean noTitle: type: boolean password: - description: Corresponds to -p, not --userpass type: string pauseOnActive: type: integer @@ -131,7 +125,6 @@ definitions: submit: type: boolean syslog: - description: Logging options type: boolean threads: type: integer @@ -144,7 +137,6 @@ definitions: userAgent: type: string userPass: - description: Corresponds to -O type: string verbose: type: boolean @@ -215,24 +207,18 @@ definitions: configPath: type: string hashrateHistory: - description: High-resolution (10s) items: $ref: '#/definitions/mining.HashratePoint' type: array - lastHeartbeat: - type: integer lowResHashrateHistory: - description: Low-resolution (1m averages) items: $ref: '#/definitions/mining.HashratePoint' type: array miner_binary: - description: New field for the full path to the miner executable type: string name: type: string path: - description: This will now be the versioned folder path type: string running: type: boolean diff --git a/pkg/mining/manager.go b/pkg/mining/manager.go index 3affb7e..3a5a94f 100644 --- a/pkg/mining/manager.go +++ b/pkg/mining/manager.go @@ -3,6 +3,8 @@ package mining import ( "fmt" "log" + "net" + "strconv" "strings" "sync" "time" @@ -22,16 +24,6 @@ type Manager struct { var _ ManagerInterface = (*Manager)(nil) // NewManager creates a new miner manager. -// It initializes the manager and starts a background goroutine for periodic -// statistics collection from the miners. -// -// Example: -// -// // Create a new manager -// manager := mining.NewManager() -// defer manager.Stop() -// -// // Now you can use the manager to start and stop miners func NewManager() *Manager { m := &Manager{ miners: make(map[string]Miner), @@ -42,37 +34,31 @@ func NewManager() *Manager { return m } +// findAvailablePort finds an available TCP port on the local machine. +func findAvailablePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil +} + // StartMiner starts a new miner with the given configuration. -// It takes the miner type and a configuration object, and returns the -// created miner instance or an error if the miner could not be started. -// -// Example: -// -// // Create a new manager -// manager := mining.NewManager() -// defer manager.Stop() -// -// // Create a new configuration for the XMRig miner -// config := &mining.Config{ -// Miner: "xmrig", -// Pool: "your-pool-address", -// Wallet: "your-wallet-address", -// Threads: 4, -// TLS: true, -// } -// -// // Start the miner -// miner, err := manager.StartMiner("xmrig", config) -// if err != nil { -// log.Fatalf("Failed to start miner: %v", err) -// } -// -// // Stop the miner when you are done -// defer manager.StopMiner(miner.GetName()) func (m *Manager) StartMiner(minerType string, config *Config) (Miner, error) { m.mu.Lock() defer m.mu.Unlock() + // Prevent nil pointer panic if request body is empty + if config == nil { + config = &Config{} + } + var miner Miner switch strings.ToLower(minerType) { case "xmrig": @@ -81,19 +67,38 @@ func (m *Manager) StartMiner(minerType string, config *Config) (Miner, error) { return nil, fmt.Errorf("unsupported miner type: %s", minerType) } - // Ensure the miner's internal name is used for map key - minerKey := miner.GetName() - if _, exists := m.miners[minerKey]; exists { - return nil, fmt.Errorf("miner already started: %s", minerKey) + instanceName := miner.GetName() + if config.Algo != "" { + instanceName = fmt.Sprintf("%s-%s", instanceName, config.Algo) + } else { + instanceName = fmt.Sprintf("%s-%d", instanceName, time.Now().UnixNano()%1000) + } + + if _, exists := m.miners[instanceName]; exists { + return nil, fmt.Errorf("a miner with a similar configuration is already running: %s", instanceName) + } + + apiPort, err := findAvailablePort() + if err != nil { + return nil, fmt.Errorf("failed to find an available port for the miner API: %w", err) + } + if config.HTTPPort == 0 { + config.HTTPPort = apiPort + } + + if xmrigMiner, ok := miner.(*XMRigMiner); ok { + xmrigMiner.Name = instanceName + if xmrigMiner.API != nil { + xmrigMiner.API.ListenPort = apiPort + } } if err := miner.Start(config); err != nil { return nil, err } - m.miners[minerKey] = miner + m.miners[instanceName] = miner - // Log to syslog (or standard log on Windows) logMessage := fmt.Sprintf("CryptoCurrency Miner started: %s (Binary: %s)", miner.GetName(), miner.GetBinaryPath()) logToSyslog(logMessage) @@ -101,14 +106,22 @@ func (m *Manager) StartMiner(minerType string, config *Config) (Miner, error) { } // StopMiner stops a running miner. -// It takes the name of the miner to be stopped and returns an error if the -// miner could not be stopped. func (m *Manager) StopMiner(name string) error { m.mu.Lock() defer m.mu.Unlock() - minerKey := strings.ToLower(name) // Normalize input name to lowercase - miner, exists := m.miners[minerKey] + miner, exists := m.miners[name] + if !exists { + for k := range m.miners { + if strings.HasPrefix(k, name) { + miner = m.miners[k] + name = k + exists = true + break + } + } + } + if !exists { return fmt.Errorf("miner not found: %s", name) } @@ -117,18 +130,16 @@ func (m *Manager) StopMiner(name string) error { return err } - delete(m.miners, minerKey) + delete(m.miners, name) return nil } // GetMiner retrieves a running miner by its name. -// It returns the miner instance or an error if the miner is not found. func (m *Manager) GetMiner(name string) (Miner, error) { m.mu.RLock() defer m.mu.RUnlock() - minerKey := strings.ToLower(name) // Normalize input name to lowercase - miner, exists := m.miners[minerKey] + miner, exists := m.miners[name] if !exists { return nil, fmt.Errorf("miner not found: %s", name) } @@ -148,7 +159,6 @@ func (m *Manager) ListMiners() []Miner { } // ListAvailableMiners returns a list of available miners that can be started. -// This provides a way to discover the types of miners supported by the manager. func (m *Manager) ListAvailableMiners() []AvailableMiner { return []AvailableMiner{ { @@ -163,7 +173,7 @@ func (m *Manager) startStatsCollection() { m.waitGroup.Add(1) go func() { defer m.waitGroup.Done() - ticker := time.NewTicker(HighResolutionInterval) // Collect stats every 10 seconds + ticker := time.NewTicker(HighResolutionInterval) defer ticker.Stop() for { @@ -190,7 +200,6 @@ func (m *Manager) collectMinerStats() { for _, miner := range minersToCollect { stats, err := miner.GetStats() if err != nil { - // Log the error but don't stop the collection for other miners log.Printf("Error getting stats for miner %s: %v\n", miner.GetName(), err) continue } @@ -198,19 +207,16 @@ func (m *Manager) collectMinerStats() { Timestamp: now, Hashrate: stats.Hashrate, }) - miner.ReduceHashrateHistory(now) // Call the reducer + miner.ReduceHashrateHistory(now) } } // GetMinerHashrateHistory returns the hashrate history for a specific miner. -// It takes the name of the miner and returns a slice of hashrate points -// or an error if the miner is not found. func (m *Manager) GetMinerHashrateHistory(name string) ([]HashratePoint, error) { m.mu.RLock() defer m.mu.RUnlock() - minerKey := strings.ToLower(name) - miner, exists := m.miners[minerKey] + miner, exists := m.miners[name] if !exists { return nil, fmt.Errorf("miner not found: %s", name) } @@ -218,9 +224,12 @@ func (m *Manager) GetMinerHashrateHistory(name string) ([]HashratePoint, error) } // Stop stops the manager and its background goroutines. -// It should be called when the manager is no longer needed to ensure a -// graceful shutdown of the statistics collection goroutine. func (m *Manager) Stop() { close(m.stopChan) - m.waitGroup.Wait() // Wait for the stats collection goroutine to finish + m.waitGroup.Wait() +} + +// Helper to convert port to string for net.JoinHostPort +func portToString(port int) string { + return strconv.Itoa(port) } diff --git a/pkg/mining/miner.go b/pkg/mining/miner.go new file mode 100644 index 0000000..c228752 --- /dev/null +++ b/pkg/mining/miner.go @@ -0,0 +1,385 @@ +package mining + +import ( + "archive/tar" + "archive/zip" + "bytes" + "compress/gzip" + "errors" + "fmt" + "io" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "sort" + "strings" + "sync" + "time" + + "github.com/adrg/xdg" +) + +// BaseMiner provides a foundation for specific miner implementations. +type BaseMiner struct { + Name string `json:"name"` + Version string `json:"version"` + URL string `json:"url"` + Path string `json:"path"` + MinerBinary string `json:"miner_binary"` + ExecutableName string `json:"-"` + Running bool `json:"running"` + ConfigPath string `json:"configPath"` + API *API `json:"api"` + mu sync.RWMutex + cmd *exec.Cmd + HashrateHistory []HashratePoint `json:"hashrateHistory"` + LowResHashrateHistory []HashratePoint `json:"lowResHashrateHistory"` + LastLowResAggregation time.Time `json:"-"` +} + +// GetName returns the name of the miner. +func (b *BaseMiner) GetName() string { + b.mu.RLock() + defer b.mu.RUnlock() + return b.Name +} + +// GetPath returns the base installation directory for the miner type. +// It uses the stable ExecutableName field to ensure the correct path. +func (b *BaseMiner) GetPath() string { + dataPath, err := xdg.DataFile(fmt.Sprintf("lethean-desktop/miners/%s", b.ExecutableName)) + if err != nil { + home, err := os.UserHomeDir() + if err != nil { + return "" + } + return filepath.Join(home, ".lethean-desktop", "miners", b.ExecutableName) + } + return dataPath +} + +// GetBinaryPath returns the full path to the miner's executable file. +func (b *BaseMiner) GetBinaryPath() string { + b.mu.RLock() + defer b.mu.RUnlock() + return b.MinerBinary +} + +// Stop terminates the miner process. +func (b *BaseMiner) Stop() error { + b.mu.Lock() + defer b.mu.Unlock() + + if !b.Running || b.cmd == nil { + return errors.New("miner is not running") + } + + return b.cmd.Process.Kill() +} + +// Uninstall removes all files related to the miner. +func (b *BaseMiner) Uninstall() error { + return os.RemoveAll(b.GetPath()) +} + +// InstallFromURL handles the generic download and extraction process for a miner. +func (b *BaseMiner) InstallFromURL(url string) error { + tmpfile, err := os.CreateTemp("", b.ExecutableName+"-") + if err != nil { + return err + } + defer os.Remove(tmpfile.Name()) + defer tmpfile.Close() + + resp, err := httpClient.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to download release: unexpected status code %d", resp.StatusCode) + } + + if _, err := io.Copy(tmpfile, resp.Body); err != nil { + return err + } + + baseInstallPath := b.GetPath() + if err := os.MkdirAll(baseInstallPath, 0755); err != nil { + return err + } + + if strings.HasSuffix(url, ".zip") { + err = b.unzip(tmpfile.Name(), baseInstallPath) + } else { + err = b.untar(tmpfile.Name(), baseInstallPath) + } + if err != nil { + return fmt.Errorf("failed to extract miner: %w", err) + } + + return nil +} + +// findMinerBinary searches for the miner's executable file. +func (b *BaseMiner) findMinerBinary() (string, error) { + executableName := b.ExecutableName + if runtime.GOOS == "windows" { + executableName += ".exe" + } + + baseInstallPath := b.GetPath() + searchedPaths := []string{} + + if _, err := os.Stat(baseInstallPath); err == nil { + var latestModTime time.Time + var latestDir string + + dirs, err := os.ReadDir(baseInstallPath) + if err == nil { + for _, d := range dirs { + if d.IsDir() && strings.HasPrefix(d.Name(), b.ExecutableName+"-") { + versionedPath := filepath.Join(baseInstallPath, d.Name()) + fullPath := filepath.Join(versionedPath, executableName) + searchedPaths = append(searchedPaths, fullPath) + info, err := d.Info() + if err == nil && info.ModTime().After(latestModTime) { + latestModTime = info.ModTime() + latestDir = d.Name() + } + } + } + } + + if latestDir != "" { + fullPath := filepath.Join(baseInstallPath, latestDir, executableName) + if _, err := os.Stat(fullPath); err == nil { + log.Printf("Found miner binary at standard path: %s", fullPath) + return fullPath, nil + } + } + } + + path, err := exec.LookPath(executableName) + if err == nil { + absPath, err := filepath.Abs(path) + if err != nil { + return "", fmt.Errorf("failed to get absolute path for '%s': %w", path, err) + } + log.Printf("Found miner binary in system PATH: %s", absPath) + return absPath, nil + } + + return "", fmt.Errorf("miner executable '%s' not found. Searched in: %s and system PATH", executableName, strings.Join(searchedPaths, ", ")) +} + +// CheckInstallation verifies if the miner is installed correctly. +func (b *BaseMiner) CheckInstallation() (*InstallationDetails, error) { + binaryPath, err := b.findMinerBinary() + if err != nil { + return &InstallationDetails{IsInstalled: false}, err + } + + b.MinerBinary = binaryPath + b.Path = filepath.Dir(binaryPath) + + cmd := exec.Command(binaryPath, "--version") + var out bytes.Buffer + cmd.Stdout = &out + if err := cmd.Run(); err != nil { + b.Version = "Unknown (could not run executable)" + } else { + fields := strings.Fields(out.String()) + if len(fields) >= 2 { + b.Version = fields[1] + } else { + b.Version = "Unknown (could not parse version)" + } + } + + return &InstallationDetails{ + IsInstalled: true, + MinerBinary: b.MinerBinary, + Path: b.Path, + Version: b.Version, + }, nil +} + +// GetHashrateHistory returns the combined hashrate history. +func (b *BaseMiner) GetHashrateHistory() []HashratePoint { + b.mu.RLock() + defer b.mu.RUnlock() + combinedHistory := make([]HashratePoint, 0, len(b.LowResHashrateHistory)+len(b.HashrateHistory)) + combinedHistory = append(combinedHistory, b.LowResHashrateHistory...) + combinedHistory = append(combinedHistory, b.HashrateHistory...) + return combinedHistory +} + +// AddHashratePoint adds a new hashrate measurement. +func (b *BaseMiner) AddHashratePoint(point HashratePoint) { + b.mu.Lock() + defer b.mu.Unlock() + b.HashrateHistory = append(b.HashrateHistory, point) +} + +// ReduceHashrateHistory aggregates and trims hashrate data. +func (b *BaseMiner) ReduceHashrateHistory(now time.Time) { + b.mu.Lock() + defer b.mu.Unlock() + + if !b.LastLowResAggregation.IsZero() && now.Sub(b.LastLowResAggregation) < LowResolutionInterval { + return + } + + var pointsToAggregate []HashratePoint + var newHighResHistory []HashratePoint + cutoff := now.Add(-HighResolutionDuration) + + for _, p := range b.HashrateHistory { + if p.Timestamp.Before(cutoff) { + pointsToAggregate = append(pointsToAggregate, p) + } else { + newHighResHistory = append(newHighResHistory, p) + } + } + b.HashrateHistory = newHighResHistory + + if len(pointsToAggregate) == 0 { + b.LastLowResAggregation = now + return + } + + minuteGroups := make(map[time.Time][]int) + for _, p := range pointsToAggregate { + minute := p.Timestamp.Truncate(LowResolutionInterval) + minuteGroups[minute] = append(minuteGroups[minute], p.Hashrate) + } + + var newLowResPoints []HashratePoint + for minute, hashrates := range minuteGroups { + if len(hashrates) > 0 { + totalHashrate := 0 + for _, hr := range hashrates { + totalHashrate += hr + } + avgHashrate := totalHashrate / len(hashrates) + newLowResPoints = append(newLowResPoints, HashratePoint{Timestamp: minute, Hashrate: avgHashrate}) + } + } + + sort.Slice(newLowResPoints, func(i, j int) bool { + return newLowResPoints[i].Timestamp.Before(newLowResPoints[j].Timestamp) + }) + + b.LowResHashrateHistory = append(b.LowResHashrateHistory, newLowResPoints...) + + lowResCutoff := now.Add(-LowResHistoryRetention) + firstValidLowResIndex := 0 + for i, p := range b.LowResHashrateHistory { + if p.Timestamp.After(lowResCutoff) || p.Timestamp.Equal(lowResCutoff) { + firstValidLowResIndex = i + break + } + if i == len(b.LowResHashrateHistory)-1 { + firstValidLowResIndex = len(b.LowResHashrateHistory) + } + } + b.LowResHashrateHistory = b.LowResHashrateHistory[firstValidLowResIndex:] + b.LastLowResAggregation = now +} + +// unzip extracts a zip archive. +func (b *BaseMiner) unzip(src, dest string) error { + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + fpath := filepath.Join(dest, f.Name) + if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { + return fmt.Errorf("%s: illegal file path", fpath) + } + if f.FileInfo().IsDir() { + os.MkdirAll(fpath, os.ModePerm) + continue + } + + if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + return err + } + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + rc, err := f.Open() + if err != nil { + outFile.Close() + return err + } + _, err = io.Copy(outFile, rc) + outFile.Close() + rc.Close() + if err != nil { + return err + } + } + return nil +} + +// untar extracts a tar.gz archive. +func (b *BaseMiner) untar(src, dest string) error { + file, err := os.Open(src) + if err != nil { + return err + } + defer file.Close() + + gzr, err := gzip.NewReader(file) + if err != nil { + return err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + + for { + header, err := tr.Next() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + target := filepath.Join(dest, header.Name) + if !strings.HasPrefix(target, filepath.Clean(dest)+string(os.PathSeparator)) { + continue + } + + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(target, 0755); err != nil { + return err + } + case tar.TypeReg: + if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { + return err + } + f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode)) + if err != nil { + return err + } + if _, err := io.Copy(f, tr); err != nil { + f.Close() + return err + } + f.Close() + } + } +} diff --git a/pkg/mining/mining.go b/pkg/mining/mining.go index ca9f976..423e336 100644 --- a/pkg/mining/mining.go +++ b/pkg/mining/mining.go @@ -1,8 +1,6 @@ package mining import ( - "os/exec" - "sync" "time" ) @@ -22,12 +20,7 @@ const ( // starting, stopping, and statistics retrieval, allowing for different miner // implementations to be used interchangeably. type Miner interface { - // Install handles the setup and installation of the miner software. - // This may include downloading binaries, creating configuration files, - // and setting up necessary permissions. Install() error - - // Uninstall removes the miner software and any related configuration files. Uninstall() error Start(config *Config) error Stop() error @@ -35,333 +28,138 @@ type Miner interface { GetName() string GetPath() string GetBinaryPath() string - - // CheckInstallation verifies if the miner is installed correctly and returns - // details about the installation, such as the version and path. CheckInstallation() (*InstallationDetails, error) - - // GetLatestVersion retrieves the latest available version of the miner software. GetLatestVersion() (string, error) - - // GetHashrateHistory returns the recent hashrate history of the miner. GetHashrateHistory() []HashratePoint - - // AddHashratePoint adds a new hashrate data point to the miner's history. AddHashratePoint(point HashratePoint) - - // ReduceHashrateHistory processes the raw hashrate data, potentially - // aggregating high-resolution data into a lower-resolution format for - // long-term storage. ReduceHashrateHistory(now time.Time) } // InstallationDetails contains information about an installed miner. -// It provides a standard structure for reporting the status of a miner's -// installation, including whether it's present, its version, and its location. type InstallationDetails struct { - // IsInstalled is true if the miner is installed, false otherwise. - IsInstalled bool `json:"is_installed"` - // Version is the detected version of the installed miner. - Version string `json:"version"` - // Path is the installation path of the miner. - Path string `json:"path"` - // MinerBinary is the name of the miner's executable file. + IsInstalled bool `json:"is_installed"` + Version string `json:"version"` + Path string `json:"path"` MinerBinary string `json:"miner_binary"` } // SystemInfo provides general system and miner installation information. -// This struct aggregates various details about the system's environment, -// such as operating system, architecture, and available resources, as well -// as information about installed miners. type SystemInfo struct { - // Timestamp is the time when the system information was collected. - Timestamp time.Time `json:"timestamp"` - // OS is the operating system of the host. - OS string `json:"os"` - // Architecture is the system's hardware architecture (e.g., amd64, arm64). - Architecture string `json:"architecture"` - // GoVersion is the version of the Go runtime. - GoVersion string `json:"go_version"` - // AvailableCPUCores is the number of available CPU cores. - AvailableCPUCores int `json:"available_cpu_cores"` - // TotalSystemRAMGB is the total system RAM in gigabytes. - TotalSystemRAMGB float64 `json:"total_system_ram_gb"` - // InstalledMinersInfo is a slice containing details of all installed miners. + Timestamp time.Time `json:"timestamp"` + OS string `json:"os"` + Architecture string `json:"architecture"` + GoVersion string `json:"go_version"` + AvailableCPUCores int `json:"available_cpu_cores"` + TotalSystemRAMGB float64 `json:"total_system_ram_gb"` InstalledMinersInfo []*InstallationDetails `json:"installed_miners_info"` } // Config represents the configuration for a miner. -// This struct includes general mining parameters as well as specific options -// for different miner implementations like XMRig. It is designed to be - -// flexible and comprehensive, covering a wide range of settings from network -// and CPU configurations to logging and miscellaneous options. -// -// Example: -// -// // Create a new configuration for the XMRig miner -// config := &mining.Config{ -// Miner: "xmrig", -// Pool: "your-pool-address", -// Wallet: "your-wallet-address", -// Threads: 4, -// TLS: true, -// } type Config struct { - // Miner is the name of the miner to be used (e.g., "xmrig"). - Miner string `json:"miner"` - // Pool is the address of the mining pool. - Pool string `json:"pool"` - // Wallet is the user's wallet address for receiving mining rewards. - Wallet string `json:"wallet"` - // Threads is the number of CPU threads to be used for mining. - Threads int `json:"threads"` - // TLS indicates whether to use a secure TLS connection to the pool. - TLS bool `json:"tls"` - // HugePages enables or disables the use of huge pages for performance optimization. - HugePages bool `json:"hugePages"` - - // Network options - // Algo specifies the mining algorithm to be used. - Algo string `json:"algo,omitempty"` - // Coin specifies the cryptocurrency to be mined. - Coin string `json:"coin,omitempty"` - // Password is the pool password. - Password string `json:"password,omitempty"` - // UserPass is the username and password for the pool. - UserPass string `json:"userPass,omitempty"` - // Proxy is the address of a proxy to be used for the connection. - Proxy string `json:"proxy,omitempty"` - // Keepalive enables or disables the TCP keepalive feature. - Keepalive bool `json:"keepalive,omitempty"` - // Nicehash enables or disables Nicehash support. - Nicehash bool `json:"nicehash,omitempty"` - // RigID is the identifier of the mining rig. - RigID string `json:"rigId,omitempty"` - // TLSSingerprint is the TLS fingerprint of the pool. - TLSSingerprint string `json:"tlsFingerprint,omitempty"` - // Retries is the number of times to retry connecting to the pool. - Retries int `json:"retries,omitempty"` - // RetryPause is the pause in seconds between connection retries. - RetryPause int `json:"retryPause,omitempty"` - // UserAgent is the user agent string to be used for the connection. - UserAgent string `json:"userAgent,omitempty"` - // DonateLevel is the donation level to the miner developers. - DonateLevel int `json:"donateLevel,omitempty"` - // DonateOverProxy enables or disables donation over a proxy. - DonateOverProxy bool `json:"donateOverProxy,omitempty"` - - // CPU backend options - // NoCPU disables the CPU backend. - NoCPU bool `json:"noCpu,omitempty"` - // CPUAffinity sets the CPU affinity for the miner. - CPUAffinity string `json:"cpuAffinity,omitempty"` - // AV is the algorithm variation. - AV int `json:"av,omitempty"` - // CPUPriority is the CPU priority for the miner. - CPUPriority int `json:"cpuPriority,omitempty"` - // CPUMaxThreadsHint is the maximum number of threads hint for the CPU. - CPUMaxThreadsHint int `json:"cpuMaxThreadsHint,omitempty"` - // CPUMemoryPool is the CPU memory pool size. - CPUMemoryPool int `json:"cpuMemoryPool,omitempty"` - // CPUNoYield enables or disables CPU yield. - CPUNoYield bool `json:"cpuNoYield,omitempty"` - // HugepageSize is the size of huge pages in kilobytes. - HugepageSize int `json:"hugepageSize,omitempty"` - // HugePagesJIT enables or disables huge pages for JIT compiled code. - HugePagesJIT bool `json:"hugePagesJIT,omitempty"` - // ASM enables or disables the ASM compiler. - ASM string `json:"asm,omitempty"` - // Argon2Impl is the Argon2 implementation. - Argon2Impl string `json:"argon2Impl,omitempty"` - // RandomXInit is the RandomX initialization value. - RandomXInit int `json:"randomXInit,omitempty"` - // RandomXNoNUMA enables or disables NUMA support for RandomX. - RandomXNoNUMA bool `json:"randomXNoNuma,omitempty"` - // RandomXMode is the RandomX mode. - RandomXMode string `json:"randomXMode,omitempty"` - // RandomX1GBPages enables or disables 1GB pages for RandomX. - RandomX1GBPages bool `json:"randomX1GBPages,omitempty"` - // RandomXWrmsr is the RandomX MSR value. - RandomXWrmsr string `json:"randomXWrmsr,omitempty"` - // RandomXNoRdmsr enables or disables MSR reading for RandomX. - RandomXNoRdmsr bool `json:"randomXNoRdmsr,omitempty"` - // RandomXCacheQoS enables or disables QoS for the RandomX cache. - RandomXCacheQoS bool `json:"randomXCacheQoS,omitempty"` - - // API options (can be overridden or supplemented here) - // APIWorkerID is the worker ID for the API. - APIWorkerID string `json:"apiWorkerId,omitempty"` - // APIID is the ID for the API. - APIID string `json:"apiId,omitempty"` - // HTTPHost is the host for the HTTP API. - HTTPHost string `json:"httpHost,omitempty"` - // HTTPPort is the port for the HTTP API. - HTTPPort int `json:"httpPort,omitempty"` - // HTTPAccessToken is the access token for the HTTP API. - HTTPAccessToken string `json:"httpAccessToken,omitempty"` - // HTTPNoRestricted enables or disables restricted access to the HTTP API. - HTTPNoRestricted bool `json:"httpNoRestricted,omitempty"` - - // Logging options - // Syslog enables or disables logging to the system log. - Syslog bool `json:"syslog,omitempty"` - // LogFile is the path to the log file. - LogFile string `json:"logFile,omitempty"` - // PrintTime is the interval in seconds for printing performance metrics. - PrintTime int `json:"printTime,omitempty"` - // HealthPrintTime is the interval in seconds for printing health metrics. - HealthPrintTime int `json:"healthPrintTime,omitempty"` - // NoColor disables color output in the logs. - NoColor bool `json:"noColor,omitempty"` - // Verbose enables verbose logging. - Verbose bool `json:"verbose,omitempty"` - // LogOutput enables or disables logging of stdout/stderr. - LogOutput bool `json:"logOutput,omitempty"` - - // Misc options - // Background runs the miner in the background. - Background bool `json:"background,omitempty"` - // Title sets the title of the miner window. - Title string `json:"title,omitempty"` - // NoTitle disables the miner window title. - NoTitle bool `json:"noTitle,omitempty"` - // PauseOnBattery pauses the miner when the system is on battery power. - PauseOnBattery bool `json:"pauseOnBattery,omitempty"` - // PauseOnActive pauses the miner when the user is active. - PauseOnActive int `json:"pauseOnActive,omitempty"` - // Stress enables stress testing mode. - Stress bool `json:"stress,omitempty"` - // Bench enables benchmark mode. - Bench string `json:"bench,omitempty"` - // Submit enables or disables submitting shares. - Submit bool `json:"submit,omitempty"` - // Verify enables or disables share verification. - Verify string `json:"verify,omitempty"` - // Seed is the seed for the random number generator. - Seed string `json:"seed,omitempty"` - // Hash is the hash for the random number generator. - Hash string `json:"hash,omitempty"` - // NoDMI disables DMI/SMBIOS probing. - NoDMI bool `json:"noDMI,omitempty"` + Miner string `json:"miner"` + Pool string `json:"pool"` + Wallet string `json:"wallet"` + Threads int `json:"threads"` + TLS bool `json:"tls"` + HugePages bool `json:"hugePages"` + Algo string `json:"algo,omitempty"` + Coin string `json:"coin,omitempty"` + Password string `json:"password,omitempty"` + UserPass string `json:"userPass,omitempty"` + Proxy string `json:"proxy,omitempty"` + Keepalive bool `json:"keepalive,omitempty"` + Nicehash bool `json:"nicehash,omitempty"` + RigID string `json:"rigId,omitempty"` + TLSSingerprint string `json:"tlsFingerprint,omitempty"` + Retries int `json:"retries,omitempty"` + RetryPause int `json:"retryPause,omitempty"` + UserAgent string `json:"userAgent,omitempty"` + DonateLevel int `json:"donateLevel,omitempty"` + DonateOverProxy bool `json:"donateOverProxy,omitempty"` + NoCPU bool `json:"noCpu,omitempty"` + CPUAffinity string `json:"cpuAffinity,omitempty"` + AV int `json:"av,omitempty"` + CPUPriority int `json:"cpuPriority,omitempty"` + CPUMaxThreadsHint int `json:"cpuMaxThreadsHint,omitempty"` + CPUMemoryPool int `json:"cpuMemoryPool,omitempty"` + CPUNoYield bool `json:"cpuNoYield,omitempty"` + HugepageSize int `json:"hugepageSize,omitempty"` + HugePagesJIT bool `json:"hugePagesJIT,omitempty"` + ASM string `json:"asm,omitempty"` + Argon2Impl string `json:"argon2Impl,omitempty"` + RandomXInit int `json:"randomXInit,omitempty"` + RandomXNoNUMA bool `json:"randomXNoNuma,omitempty"` + RandomXMode string `json:"randomXMode,omitempty"` + RandomX1GBPages bool `json:"randomX1GBPages,omitempty"` + RandomXWrmsr string `json:"randomXWrmsr,omitempty"` + RandomXNoRdmsr bool `json:"randomXNoRdmsr,omitempty"` + RandomXCacheQoS bool `json:"randomXCacheQoS,omitempty"` + APIWorkerID string `json:"apiWorkerId,omitempty"` + APIID string `json:"apiId,omitempty"` + HTTPHost string `json:"httpHost,omitempty"` + HTTPPort int `json:"httpPort,omitempty"` + HTTPAccessToken string `json:"httpAccessToken,omitempty"` + HTTPNoRestricted bool `json:"httpNoRestricted,omitempty"` + Syslog bool `json:"syslog,omitempty"` + LogFile string `json:"logFile,omitempty"` + PrintTime int `json:"printTime,omitempty"` + HealthPrintTime int `json:"healthPrintTime,omitempty"` + NoColor bool `json:"noColor,omitempty"` + Verbose bool `json:"verbose,omitempty"` + LogOutput bool `json:"logOutput,omitempty"` + Background bool `json:"background,omitempty"` + Title string `json:"title,omitempty"` + NoTitle bool `json:"noTitle,omitempty"` + PauseOnBattery bool `json:"pauseOnBattery,omitempty"` + PauseOnActive int `json:"pauseOnActive,omitempty"` + Stress bool `json:"stress,omitempty"` + Bench string `json:"bench,omitempty"` + Submit bool `json:"submit,omitempty"` + Verify string `json:"verify,omitempty"` + Seed string `json:"seed,omitempty"` + Hash string `json:"hash,omitempty"` + NoDMI bool `json:"noDMI,omitempty"` } // PerformanceMetrics represents the performance metrics for a miner. -// This struct provides a standardized way to report key performance indicators -// such as hashrate, shares, and uptime, allowing for consistent monitoring -// and comparison across different miners. type PerformanceMetrics struct { - // Hashrate is the current hashrate of the miner in H/s. - Hashrate int `json:"hashrate"` - // Shares is the number of shares submitted by the miner. - Shares int `json:"shares"` - // Rejected is the number of rejected shares. - Rejected int `json:"rejected"` - // Uptime is the duration the miner has been running, in seconds. - Uptime int `json:"uptime"` - // LastShare is the timestamp of the last submitted share. - LastShare int64 `json:"lastShare"` - // Algorithm is the mining algorithm currently in use. - Algorithm string `json:"algorithm"` - // ExtraData provides a flexible way to include additional, miner-specific - // performance data that is not covered by the standard fields. + Hashrate int `json:"hashrate"` + Shares int `json:"shares"` + Rejected int `json:"rejected"` + Uptime int `json:"uptime"` + LastShare int64 `json:"lastShare"` + Algorithm string `json:"algorithm"` ExtraData map[string]interface{} `json:"extraData,omitempty"` } -// History represents the historical performance data for a miner. -// It contains a collection of performance metrics snapshots, allowing for -// the tracking of a miner's performance over time. -type History struct { - // Miner is the name of the miner. - Miner string `json:"miner"` - // Stats is a slice of performance metrics, representing the historical data. - Stats []PerformanceMetrics `json:"stats"` - // Updated is the timestamp of the last update to the history. - Updated int64 `json:"updated"` -} - // HashratePoint represents a single hashrate measurement at a specific time. -// This struct is used to build a time-series history of a miner's hashrate, -// which is essential for performance analysis and visualization. type HashratePoint struct { - // Timestamp is the time at which the hashrate was measured. Timestamp time.Time `json:"timestamp"` - // Hashrate is the measured hashrate in H/s. - Hashrate int `json:"hashrate"` + Hashrate int `json:"hashrate"` } -// XMRigMiner represents an XMRig miner, encapsulating its configuration, -// state, and operational details. This struct provides a comprehensive -// representation of an XMRig miner instance, including its identity, -// connection details, and performance history. -type XMRigMiner struct { - // Name is the name of the miner. - Name string `json:"name"` - // Version is the version of the XMRig miner. - Version string `json:"version"` - // URL is the download URL for the XMRig miner. - URL string `json:"url"` - // Path is the installation path of the miner. - Path string `json:"path"` - // MinerBinary is the full path to the miner's executable file. - MinerBinary string `json:"miner_binary"` - // Running indicates whether the miner is currently running. - Running bool `json:"running"` - // LastHeartbeat is the timestamp of the last heartbeat from the miner. - LastHeartbeat int64 `json:"lastHeartbeat"` - // ConfigPath is the path to the miner's configuration file. - ConfigPath string `json:"configPath"` - // API provides access to the miner's API for statistics and control. - API *API `json:"api"` - // mu is a mutex to protect against concurrent access to the miner's state. - mu sync.Mutex - // cmd is the command used to execute the miner process. - cmd *exec.Cmd `json:"-"` - // HashrateHistory is a slice of high-resolution hashrate data points. - HashrateHistory []HashratePoint `json:"hashrateHistory"` - // LowResHashrateHistory is a slice of low-resolution hashrate data points. - LowResHashrateHistory []HashratePoint `json:"lowResHashrateHistory"` - // LastLowResAggregation is the timestamp of the last low-resolution aggregation. - LastLowResAggregation time.Time `json:"-"` -} - -// API represents the XMRig API configuration. -// It specifies the details needed to connect to the miner's API, -// enabling programmatic monitoring and control. +// API represents the miner's API configuration. type API struct { - // Enabled indicates whether the API is enabled. - Enabled bool `json:"enabled"` - // ListenHost is the host on which the API is listening. + Enabled bool `json:"enabled"` ListenHost string `json:"listenHost"` - // ListenPort is the port on which the API is listening. - ListenPort int `json:"listenPort"` + ListenPort int `json:"listenPort"` } -// XMRigSummary represents the summary of an XMRig miner's performance, -// as retrieved from its API. This struct provides a structured way to -// access key performance indicators from the miner's API. +// XMRigSummary represents the summary of an XMRig miner's performance. type XMRigSummary struct { - // Hashrate contains the hashrate data from the API. Hashrate struct { Total []float64 `json:"total"` } `json:"hashrate"` - // Results contains the share statistics from the API. Results struct { SharesGood uint64 `json:"shares_good"` SharesTotal uint64 `json:"shares_total"` } `json:"results"` - // Uptime is the duration the miner has been running, in seconds. - Uptime uint64 `json:"uptime"` - // Algorithm is the mining algorithm currently in use. + Uptime uint64 `json:"uptime"` Algorithm string `json:"algorithm"` } // AvailableMiner represents a miner that is available for use. -// It provides a simple way to list and describe the miners that can be -// started and managed by the system. type AvailableMiner struct { - // Name is the name of the available miner. - Name string `json:"name"` - // Description is a brief description of the miner. + Name string `json:"name"` Description string `json:"description"` } diff --git a/pkg/mining/ttminer.go b/pkg/mining/ttminer.go index d8f4790..7bc3bf0 100644 --- a/pkg/mining/ttminer.go +++ b/pkg/mining/ttminer.go @@ -2,32 +2,33 @@ package mining import ( "errors" + "time" ) -// TTMiner represents a TT-Miner +// TTMiner represents a TT-Miner, embedding the BaseMiner for common functionality. type TTMiner struct { - Name string `json:"name"` - Version string `json:"version"` - URL string `json:"url"` - Path string `json:"path"` - Running bool `json:"running"` - Pid int `json:"pid"` + BaseMiner } -// NewTTMiner creates a new TT-Miner +// NewTTMiner creates a new TT-Miner instance with default settings. func NewTTMiner() *TTMiner { return &TTMiner{ - Name: "TT-Miner", - Version: "latest", - URL: "https://github.com/TrailingStop/TT-Miner-release", + BaseMiner: BaseMiner{ + Name: "tt-miner", + ExecutableName: "TT-Miner", // Or whatever the actual executable is named + Version: "latest", + URL: "https://github.com/TrailingStop/TT-Miner-release", + API: &API{ + Enabled: false, // Assuming no API for now + ListenHost: "127.0.0.1", + }, + HashrateHistory: make([]HashratePoint, 0), + LowResHashrateHistory: make([]HashratePoint, 0), + LastLowResAggregation: time.Now(), + }, } } -// GetName returns the name of the miner -func (m *TTMiner) GetName() string { - return m.Name -} - // Install the miner func (m *TTMiner) Install() error { return errors.New("not implemented") @@ -38,12 +39,22 @@ func (m *TTMiner) Start(config *Config) error { return errors.New("not implemented") } -// Stop the miner -func (m *TTMiner) Stop() error { - return errors.New("not implemented") -} - // GetStats returns the stats for the miner func (m *TTMiner) GetStats() (*PerformanceMetrics, error) { return nil, errors.New("not implemented") } + +// CheckInstallation verifies if the TT-Miner is installed correctly. +func (m *TTMiner) CheckInstallation() (*InstallationDetails, error) { + return nil, errors.New("not implemented") +} + +// GetLatestVersion retrieves the latest available version of the TT-Miner. +func (m *TTMiner) GetLatestVersion() (string, error) { + return "", errors.New("not implemented") +} + +// Uninstall removes all files related to the TT-Miner. +func (m *TTMiner) Uninstall() error { + return errors.New("not implemented") +} diff --git a/pkg/mining/xmrig.go b/pkg/mining/xmrig.go index d9763a5..7b71719 100644 --- a/pkg/mining/xmrig.go +++ b/pkg/mining/xmrig.go @@ -1,82 +1,44 @@ package mining import ( - "archive/tar" - "archive/zip" - "bytes" - "compress/gzip" "encoding/json" "errors" "fmt" - "io" - "log" "net/http" - "os" - "os/exec" - "path/filepath" "runtime" - "sort" "strings" "time" - - "github.com/adrg/xdg" ) +// XMRigMiner represents an XMRig miner, embedding the BaseMiner for common functionality. +type XMRigMiner struct { + BaseMiner +} + var httpClient = &http.Client{ Timeout: 30 * time.Second, } // NewXMRigMiner creates a new XMRig miner instance with default settings. -// This is the entry point for creating a new XMRig miner that can be managed -// by the Manager. The returned miner is ready to be installed and started. -// -// Example: -// -// // Create a new XMRig miner -// xmrigMiner := mining.NewXMRigMiner() -// -// // Now you can use the miner to perform actions like -// // installing, starting, and stopping. func NewXMRigMiner() *XMRigMiner { return &XMRigMiner{ - Name: "xmrig", - Version: "latest", - URL: "https://github.com/xmrig/xmrig/releases", - API: &API{ - Enabled: true, - ListenHost: "127.0.0.1", - ListenPort: 9000, + BaseMiner: BaseMiner{ + Name: "xmrig", + ExecutableName: "xmrig", + Version: "latest", + URL: "https://github.com/xmrig/xmrig/releases", + API: &API{ + Enabled: true, + ListenHost: "127.0.0.1", + }, + HashrateHistory: make([]HashratePoint, 0), + LowResHashrateHistory: make([]HashratePoint, 0), + LastLowResAggregation: time.Now(), }, - HashrateHistory: make([]HashratePoint, 0), - LowResHashrateHistory: make([]HashratePoint, 0), - LastLowResAggregation: time.Now(), } } -// GetName returns the name of the miner. -func (m *XMRigMiner) GetName() string { - return m.Name -} - -// GetPath returns the base installation directory for the XMRig miner. -// This is the directory where different versions of the miner are stored. -func (m *XMRigMiner) GetPath() string { - dataPath, err := xdg.DataFile("lethean-desktop/miners/xmrig") - if err != nil { - return "" - } - return dataPath -} - -// GetBinaryPath returns the full path to the miner's executable file. -// This path is set after a successful installation or check of the installation status. -func (m *XMRigMiner) GetBinaryPath() string { - return m.MinerBinary -} - // GetLatestVersion fetches the latest version of XMRig from the GitHub API. -// It returns the version string (e.g., "v6.18.0") or an error if the -// version could not be retrieved. func (m *XMRigMiner) GetLatestVersion() (string, error) { resp, err := httpClient.Get("https://api.github.com/repos/xmrig/xmrig/releases/latest") if err != nil { @@ -97,9 +59,8 @@ func (m *XMRigMiner) GetLatestVersion() (string, error) { return release.TagName, nil } -// Install downloads and installs the latest version of the XMRig miner. -// It determines the correct release for the current operating system, -// downloads it, and extracts it to the appropriate installation directory. +// Install determines the correct download URL for the latest version of XMRig +// and then calls the generic InstallFromURL method on the BaseMiner. func (m *XMRigMiner) Install() error { version, err := m.GetLatestVersion() if err != nil { @@ -107,7 +68,6 @@ func (m *XMRigMiner) Install() error { } m.Version = version - // Construct the download URL var url string switch runtime.GOOS { case "windows": @@ -120,48 +80,11 @@ func (m *XMRigMiner) Install() error { return errors.New("unsupported operating system") } - // Create a temporary file to download the release to - tmpfile, err := os.CreateTemp("", "xmrig-") - if err != nil { - return err - } - defer os.Remove(tmpfile.Name()) - defer tmpfile.Close() - - // Download the release - resp, err := httpClient.Get(url) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("failed to download release: unexpected status code %d", resp.StatusCode) - } - - if _, err := io.Copy(tmpfile, resp.Body); err != nil { + if err := m.InstallFromURL(url); err != nil { return err } - // The base installation path (e.g., .../miners/xmrig) - baseInstallPath := m.GetPath() - - // Create the base installation directory if it doesn't exist - if err := os.MkdirAll(baseInstallPath, 0755); err != nil { - return err - } - - // Extract the release - if strings.HasSuffix(url, ".zip") { - err = m.unzip(tmpfile.Name(), baseInstallPath) - } else { - err = m.untar(tmpfile.Name(), baseInstallPath) - } - if err != nil { - return fmt.Errorf("failed to extract miner: %w", err) - } - - // After extraction, call CheckInstallation to populate m.Path and m.MinerBinary correctly + // After installation, verify it. _, err = m.CheckInstallation() if err != nil { return fmt.Errorf("failed to verify installation after extraction: %w", err) @@ -169,704 +92,3 @@ func (m *XMRigMiner) Install() error { return nil } - -// Uninstall removes all files related to the XMRig miner. -// This is a destructive operation that will remove the entire installation -// directory of the miner. -func (m *XMRigMiner) Uninstall() error { - return os.RemoveAll(m.GetPath()) -} - -// findMinerBinary searches for the miner's executable file. -// It first checks the standard installation path, and if not found, falls -// back to searching the system's PATH. This allows for both managed -// installations and pre-existing installations to be used. -func (m *XMRigMiner) findMinerBinary() (string, error) { - executableName := "xmrig" - if runtime.GOOS == "windows" { - executableName += ".exe" - } - - // 1. Check the standard installation directory first - baseInstallPath := m.GetPath() - if _, err := os.Stat(baseInstallPath); err == nil { - files, err := os.ReadDir(baseInstallPath) - if err == nil { - for _, f := range files { - if f.IsDir() && strings.HasPrefix(f.Name(), "xmrig-") { - versionedPath := filepath.Join(baseInstallPath, f.Name()) - fullPath := filepath.Join(versionedPath, executableName) - if _, err := os.Stat(fullPath); err == nil { - log.Printf("Found miner binary at standard path: %s", fullPath) - return fullPath, nil - } - } - } - } - } - - // 2. Fallback to searching the system PATH - path, err := exec.LookPath(executableName) - if err == nil { - log.Printf("Found miner binary in system PATH: %s", path) - return path, nil - } - - return "", errors.New("miner executable not found in standard directory or system PATH") -} - -// CheckInstallation verifies if the XMRig miner is installed correctly. -// It returns details about the installation, such as whether it is installed, -// its version, and the path to the executable. This method also updates the -// miner's internal state with the installation details. -func (m *XMRigMiner) CheckInstallation() (*InstallationDetails, error) { - details := &InstallationDetails{} - - binaryPath, err := m.findMinerBinary() - if err != nil { - details.IsInstalled = false - return details, nil // Return not-installed, but no error - } - - details.IsInstalled = true - details.MinerBinary = binaryPath - details.Path = filepath.Dir(binaryPath) // The directory containing the executable - - // Try to get the version from the executable - cmd := exec.Command(binaryPath, "--version") - var out bytes.Buffer - cmd.Stdout = &out - if err := cmd.Run(); err != nil { - details.Version = "Unknown (could not run executable)" - } else { - // XMRig version output is typically "XMRig 6.18.0" - fields := strings.Fields(out.String()) - if len(fields) >= 2 { - details.Version = fields[1] - } else { - details.Version = "Unknown (could not parse version)" - } - } - - // Update the XMRigMiner struct's fields - m.Path = details.Path - m.MinerBinary = details.MinerBinary - m.Version = details.Version // Keep the miner's version in sync - - return details, nil -} - -// Start launches the XMRig miner with the specified configuration. -// It creates a configuration file, constructs the necessary command-line -// arguments, and starts the miner process in the background. -// -// Example: -// -// // Create a new XMRig miner and a configuration -// xmrigMiner := mining.NewXMRigMiner() -// config := &mining.Config{ -// Pool: "your-pool-address", -// Wallet: "your-wallet-address", -// Threads: 4, -// } -// -// // Start the miner -// err := xmrigMiner.Start(config) -// if err != nil { -// log.Fatalf("Failed to start miner: %v", err) -// } -// -// // Stop the miner when you are done -// defer xmrigMiner.Stop() -func (m *XMRigMiner) Start(config *Config) error { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Running { - return errors.New("miner is already running") - } - - // Ensure MinerBinary is set before starting - if m.MinerBinary == "" { - // Re-check installation to populate MinerBinary if it's not set - _, err := m.CheckInstallation() - if err != nil { - return fmt.Errorf("failed to verify miner installation before starting: %w", err) - } - if m.MinerBinary == "" { - return errors.New("miner executable path not found") - } - } - - if _, err := os.Stat(m.MinerBinary); os.IsNotExist(err) { - return fmt.Errorf("xmrig executable not found at %s", m.MinerBinary) - } - - // Create the config file (this handles pool, wallet, threads, hugepages, tls, and API settings) - if err := m.createConfig(config); err != nil { - return err - } - - // Arguments for XMRig - args := []string{ - "-c", m.ConfigPath, // Always use the generated config file - } - - // Dynamically add command-line arguments based on the Config struct - // Network options - // Pool and Wallet are primarily handled by the config file, but CLI can override - if config.Pool != "" { - args = append(args, "-o", config.Pool) - } - if config.Wallet != "" { - args = append(args, "-u", config.Wallet) - } - if config.Algo != "" { - args = append(args, "-a", config.Algo) - } - if config.Coin != "" { - args = append(args, "--coin", config.Coin) - } - if config.Password != "" { - args = append(args, "-p", config.Password) - } - if config.UserPass != "" { - args = append(args, "-O", config.UserPass) - } - if config.Proxy != "" { - args = append(args, "-x", config.Proxy) - } - if config.Keepalive { - args = append(args, "-k") - } - if config.Nicehash { - args = append(args, "--nicehash") - } - if config.RigID != "" { - args = append(args, "--rig-id", config.RigID) - } - // TLS is handled by config file, but --tls-fingerprint is a CLI option - //if config.TLS { // If TLS is true in config, ensure --tls is passed if not already in config file - args = append(args, "--tls") - //} - if config.TLSSingerprint != "" { - args = append(args, "--tls-fingerprint", config.TLSSingerprint) - } - if config.Retries != 0 { - args = append(args, "-r", fmt.Sprintf("%d", config.Retries)) - } - if config.RetryPause != 0 { - args = append(args, "-R", fmt.Sprintf("%d", config.RetryPause)) - } - if config.UserAgent != "" { - args = append(args, "--user-agent", config.UserAgent) - } - if config.DonateLevel != 0 { - args = append(args, "--donate-level", fmt.Sprintf("%d", config.DonateLevel)) - } - if config.DonateOverProxy { - args = append(args, "--donate-over-proxy") - } - - // CPU backend options - if config.NoCPU { - args = append(args, "--no-cpu") - } - // Threads is handled by config file, but can be overridden by CLI - if config.Threads != 0 { // This will override the config file setting if provided - args = append(args, "-t", fmt.Sprintf("%d", config.Threads)) - } - if config.CPUAffinity != "" { - args = append(args, "--cpu-affinity", config.CPUAffinity) - } - if config.AV != 0 { - args = append(args, "-v", fmt.Sprintf("%d", config.AV)) - } - if config.CPUPriority != 0 { - args = append(args, "--cpu-priority", fmt.Sprintf("%d", config.CPUPriority)) - } - if config.CPUMaxThreadsHint != 0 { - args = append(args, "--cpu-max-threads-hint", fmt.Sprintf("%d", config.CPUMaxThreadsHint)) - } - if config.CPUMemoryPool != 0 { - args = append(args, "--cpu-memory-pool", fmt.Sprintf("%d", config.CPUMemoryPool)) - } - if config.CPUNoYield { - args = append(args, "--cpu-no-yield") - } - if !config.HugePages { // If HugePages is explicitly false in config, add --no-huge-pages - args = append(args, "--no-huge-pages") - } - if config.HugepageSize != 0 { - args = append(args, "--hugepage-size", fmt.Sprintf("%d", config.HugepageSize)) - } - if config.HugePagesJIT { - args = append(args, "--huge-pages-jit") - } - if config.ASM != "" { - args = append(args, "--asm", config.ASM) - } - if config.Argon2Impl != "" { - args = append(args, "--argon2-impl", config.Argon2Impl) - } - if config.RandomXInit != 0 { - args = append(args, "--randomx-init", fmt.Sprintf("%d", config.RandomXInit)) - } - if config.RandomXNoNUMA { - args = append(args, "--randomx-no-numa") - } - if config.RandomXMode != "" { - args = append(args, "--randomx-mode", config.RandomXMode) - } - if config.RandomX1GBPages { - args = append(args, "--randomx-1gb-pages") - } - if config.RandomXWrmsr != "" { - args = append(args, "--randomx-wrmsr", config.RandomXWrmsr) - } - if config.RandomXNoRdmsr { - args = append(args, "--randomx-no-rdmsr") - } - if config.RandomXCacheQoS { - args = append(args, "--randomx-cache-qos") - } - - // API options (CLI options override config file and m.API defaults) - if m.API.Enabled { - if config.APIWorkerID != "" { - args = append(args, "--api-worker-id", config.APIWorkerID) - } - if config.APIID != "" { - args = append(args, "--api-id", config.APIID) - } - if config.HTTPHost != "" { - args = append(args, "--http-host", config.HTTPHost) - } else { - args = append(args, "--http-host", m.API.ListenHost) - } - if config.HTTPPort != 0 { - args = append(args, "--http-port", fmt.Sprintf("%d", config.HTTPPort)) - } else { - args = append(args, "--http-port", fmt.Sprintf("%d", m.API.ListenPort)) - } - if config.HTTPAccessToken != "" { - args = append(args, "--http-access-token", config.HTTPAccessToken) - } - if config.HTTPNoRestricted { - args = append(args, "--http-no-restricted") - } - } - - // Logging options - if config.Syslog { - args = append(args, "-S") - } - if config.LogFile != "" { - args = append(args, "-l", config.LogFile) - } - if config.PrintTime != 0 { - args = append(args, "--print-time", fmt.Sprintf("%d", config.PrintTime)) - } - if config.HealthPrintTime != 0 { - args = append(args, "--health-print-time", fmt.Sprintf("%d", config.HealthPrintTime)) - } - if config.NoColor { - args = append(args, "--no-color") - } - if config.Verbose { - args = append(args, "--verbose") - } - - // Misc options - if config.Background { - args = append(args, "-B") - } - if config.Title != "" { - args = append(args, "--title", config.Title) - } - if config.NoTitle { - args = append(args, "--no-title") - } - if config.PauseOnBattery { - args = append(args, "--pause-on-battery") - } - if config.PauseOnActive != 0 { - args = append(args, "--pause-on-active", fmt.Sprintf("%d", config.PauseOnActive)) - } - if config.Stress { - args = append(args, "--stress") - } - if config.Bench != "" { - args = append(args, "--bench", config.Bench) - } - if config.Submit { - args = append(args, "--submit") - } - if config.Verify != "" { - args = append(args, "--verify", config.Verify) - } - if config.Seed != "" { - args = append(args, "--seed", config.Seed) - } - if config.Hash != "" { - args = append(args, "--hash", config.Hash) - } - if config.NoDMI { - args = append(args, "--no-dmi") - } - - fmt.Fprintf(os.Stderr, "Executing XMRig command: %s %s\n", m.MinerBinary, strings.Join(args, " ")) - - m.cmd = exec.Command(m.MinerBinary, args...) - - if config.LogOutput { - m.cmd.Stdout = os.Stdout - m.cmd.Stderr = os.Stderr - } - - if err := m.cmd.Start(); err != nil { - return err - } - - m.Running = true - - go func() { - m.cmd.Wait() - m.mu.Lock() - m.Running = false - m.cmd = nil - m.mu.Unlock() - }() - - return nil -} - -// Stop terminates the XMRig miner process. -// It sends a kill signal to the running miner process. -func (m *XMRigMiner) Stop() error { - m.mu.Lock() - defer m.mu.Unlock() - - if !m.Running || m.cmd == nil { - return errors.New("miner is not running") - } - - return m.cmd.Process.Kill() -} - -// GetStats retrieves the performance statistics from the running XMRig miner. -// It queries the miner's API and returns a PerformanceMetrics struct -// containing the hashrate, share counts, and uptime. -func (m *XMRigMiner) GetStats() (*PerformanceMetrics, error) { - m.mu.Lock() - running := m.Running - m.mu.Unlock() - - if !running { - return nil, errors.New("miner is not running") - } - - resp, err := httpClient.Get(fmt.Sprintf("http://%s:%d/2/summary", m.API.ListenHost, m.API.ListenPort)) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("failed to get stats: unexpected status code %d", resp.StatusCode) - } - - var summary XMRigSummary - if err := json.NewDecoder(resp.Body).Decode(&summary); err != nil { - return nil, err - } - - var hashrate int - if len(summary.Hashrate.Total) > 0 { - hashrate = int(summary.Hashrate.Total[0]) - } - - return &PerformanceMetrics{ - Hashrate: hashrate, - Shares: int(summary.Results.SharesGood), - Rejected: int(summary.Results.SharesTotal - summary.Results.SharesGood), - Uptime: int(summary.Uptime), - Algorithm: summary.Algorithm, - }, nil -} - -// GetHashrateHistory returns the combined high-resolution and low-resolution hashrate history. -// This provides a complete view of the miner's performance over time. -func (m *XMRigMiner) GetHashrateHistory() []HashratePoint { - m.mu.Lock() - defer m.mu.Unlock() - - // Combine low-res and high-res history - combinedHistory := make([]HashratePoint, 0, len(m.LowResHashrateHistory)+len(m.HashrateHistory)) - combinedHistory = append(combinedHistory, m.LowResHashrateHistory...) - combinedHistory = append(combinedHistory, m.HashrateHistory...) - - return combinedHistory -} - -// AddHashratePoint adds a new hashrate measurement to the high-resolution history. -// This method is called periodically by the Manager to record the miner's performance. -func (m *XMRigMiner) AddHashratePoint(point HashratePoint) { - m.mu.Lock() - defer m.mu.Unlock() - - m.HashrateHistory = append(m.HashrateHistory, point) -} - -// GetHighResHistoryLength returns the number of data points in the high-resolution hashrate history. -func (m *XMRigMiner) GetHighResHistoryLength() int { - m.mu.Lock() - defer m.mu.Unlock() - return len(m.HashrateHistory) -} - -// GetLowResHistoryLength returns the number of data points in the low-resolution hashrate history. -func (m *XMRigMiner) GetLowResHistoryLength() int { - m.mu.Lock() - defer m.mu.Unlock() - return len(m.LowResHashrateHistory) -} - -// ReduceHashrateHistory aggregates older high-resolution hashrate data into -// lower-resolution data, and trims the history to a manageable size. -// This method is called periodically by the Manager to maintain the hashrate -// history. -func (m *XMRigMiner) ReduceHashrateHistory(now time.Time) { - m.mu.Lock() - defer m.mu.Unlock() - - // Only aggregate if enough time has passed since the last aggregation - if !m.LastLowResAggregation.IsZero() && now.Sub(m.LastLowResAggregation) < LowResolutionInterval { - return - } - - var pointsToAggregate []HashratePoint - var newHighResHistory []HashratePoint - - // The cutoff is exclusive: points *at or before* this time are candidates for aggregation. - // We want to aggregate points that are *strictly older* than HighResolutionDuration ago. - cutoff := now.Add(-HighResolutionDuration) - - for _, p := range m.HashrateHistory { - if p.Timestamp.Before(cutoff) { - pointsToAggregate = append(pointsToAggregate, p) - } else { - newHighResHistory = append(newHighResHistory, p) - } - } - m.HashrateHistory = newHighResHistory - - if len(pointsToAggregate) == 0 { - m.LastLowResAggregation = now - return - } - - // Group points by minute and calculate average hashrate - minuteGroups := make(map[time.Time][]int) - for _, p := range pointsToAggregate { - minute := p.Timestamp.Truncate(LowResolutionInterval) - minuteGroups[minute] = append(minuteGroups[minute], p.Hashrate) - } - - var newLowResPoints []HashratePoint - for minute, hashrates := range minuteGroups { - if len(hashrates) > 0 { - totalHashrate := 0 - for _, hr := range hashrates { - totalHashrate += hr - } - avgHashrate := totalHashrate / len(hashrates) - newLowResPoints = append(newLowResPoints, HashratePoint{ - Timestamp: minute, - Hashrate: avgHashrate, - }) - } - } - - sort.Slice(newLowResPoints, func(i, j int) bool { - return newLowResPoints[i].Timestamp.Before(newLowResPoints[j].Timestamp) - }) - - m.LowResHashrateHistory = append(m.LowResHashrateHistory, newLowResPoints...) - - // Trim low-resolution history to LowResHistoryRetention - lowResCutoff := now.Add(-LowResHistoryRetention) - firstValidLowResIndex := 0 - for i, p := range m.LowResHashrateHistory { - if p.Timestamp.After(lowResCutoff) || p.Timestamp.Equal(lowResCutoff) { - firstValidLowResIndex = i - break - } - if i == len(m.LowResHashrateHistory)-1 { - firstValidLowResIndex = len(m.LowResHashrateHistory) - } - } - m.LowResHashrateHistory = m.LowResHashrateHistory[firstValidLowResIndex:] - - m.LastLowResAggregation = now -} - -// createConfig creates a JSON configuration file for the XMRig miner. -// This allows for a consistent and reproducible way to configure the miner, -// based on the provided Config struct. -func (m *XMRigMiner) createConfig(config *Config) error { - configPath, err := xdg.ConfigFile("lethean-desktop/xmrig.json") - if err != nil { - homeDir, err := os.UserHomeDir() - if err != nil { - return err - } - configPath = filepath.Join(homeDir, ".config", "lethean-desktop", "xmrig.json") - } - m.ConfigPath = configPath - - if err := os.MkdirAll(filepath.Dir(m.ConfigPath), 0755); err != nil { - return err - } - - c := map[string]interface{}{ - "api": map[string]interface{}{ - "enabled": m.API.Enabled, - "listen": fmt.Sprintf("%s:%d", m.API.ListenHost, m.API.ListenPort), - "access-token": nil, - "restricted": true, - }, - "pools": []map[string]interface{}{ - { - "url": config.Pool, - "user": config.Wallet, - "pass": "x", - "keepalive": true, - "tls": config.TLS, - }, - }, - "cpu": map[string]interface{}{ - "enabled": true, - "threads": config.Threads, - "huge-pages": config.HugePages, - }, - } - - data, err := json.MarshalIndent(c, "", " ") - if err != nil { - return err - } - return os.WriteFile(m.ConfigPath, data, 0644) -} - -// unzip extracts a zip archive to a destination directory. -// This is a helper function used during the installation of the miner. -func (m *XMRigMiner) unzip(src, dest string) error { - r, err := zip.OpenReader(src) - if err != nil { - return err - } - defer r.Close() - - for _, f := range r.File { - fpath := filepath.Join(dest, f.Name) - - if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { - return fmt.Errorf("%s: illegal file path", fpath) - } - - if f.FileInfo().IsDir() { - os.MkdirAll(fpath, os.ModePerm) - continue - } - - if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { - return err - } - - outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) - if err != nil { - return err - } - - rc, err := f.Open() - if err != nil { - return err - } - - _, err = io.Copy(outFile, rc) - - outFile.Close() - rc.Close() - - if err != nil { - return err - } - } - return nil -} - -// untar extracts a tar.gz archive to a destination directory. -// This is a helper function used during the installation of the miner. -func (m *XMRigMiner) untar(src, dest string) error { - file, err := os.Open(src) - if err != nil { - return err - } - defer file.Close() - - gzr, err := gzip.NewReader(file) - if err != nil { - return err - } - defer gzr.Close() - - tr := tar.NewReader(gzr) - - for { - header, err := tr.Next() - - switch { - case err == io.EOF: - return nil - case err != nil: - return err - case header == nil: - continue - } - - cleanedName := filepath.Clean(header.Name) - if strings.HasPrefix(cleanedName, "..") || strings.HasPrefix(cleanedName, "/") || cleanedName == "." { - continue - } - - target := filepath.Join(dest, cleanedName) - rel, err := filepath.Rel(dest, target) - if err != nil || strings.HasPrefix(rel, "..") { - continue - } - - switch header.Typeflag { - case tar.TypeDir: - if _, err := os.Stat(target); err != nil { - if err := os.MkdirAll(target, 0755); err != nil { - return err - } - } - case tar.TypeReg: - if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { - return err - } - f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode)) - if err != nil { - return err - } - - if _, err := io.Copy(f, tr); err != nil { - return err - } - - f.Close() - } - } -} diff --git a/pkg/mining/xmrig_start.go b/pkg/mining/xmrig_start.go new file mode 100644 index 0000000..037f63c --- /dev/null +++ b/pkg/mining/xmrig_start.go @@ -0,0 +1,154 @@ +package mining + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/adrg/xdg" +) + +// Start launches the XMRig miner with the specified configuration. +func (m *XMRigMiner) Start(config *Config) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Running { + return errors.New("miner is already running") + } + + // If the binary path isn't set, run CheckInstallation to find it. + if m.MinerBinary == "" { + if _, err := m.CheckInstallation(); err != nil { + return err // Propagate the detailed error from CheckInstallation + } + } + + 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 { + configPath, err := xdg.ConfigFile("lethean-desktop/xmrig.json") + 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) + + log.Printf("Executing XMRig command: %s %s", m.MinerBinary, strings.Join(args, " ")) + + m.cmd = exec.Command(m.MinerBinary, args...) + + if config.LogOutput { + m.cmd.Stdout = os.Stdout + m.cmd.Stderr = os.Stderr + } + + if err := m.cmd.Start(); err != nil { + return err + } + + m.Running = true + + go func() { + m.cmd.Wait() + m.mu.Lock() + m.Running = false + m.cmd = nil + m.mu.Unlock() + }() + + 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") + } +} + +// createConfig creates a JSON configuration file for the XMRig miner. +func (m *XMRigMiner) createConfig(config *Config) error { + configPath, err := xdg.ConfigFile("lethean-desktop/xmrig.json") + if err != nil { + homeDir, err := os.UserHomeDir() + if err != nil { + return err + } + configPath = filepath.Join(homeDir, ".config", "lethean-desktop", "xmrig.json") + } + 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) + } + + c := map[string]interface{}{ + "api": map[string]interface{}{ + "enabled": m.API != nil && m.API.Enabled, + "listen": apiListen, + "restricted": true, + }, + "pools": []map[string]interface{}{ + { + "url": config.Pool, + "user": config.Wallet, + "pass": "x", + "keepalive": true, + "tls": config.TLS, + }, + }, + "cpu": map[string]interface{}{ + "enabled": true, + "threads": config.Threads, + "huge-pages": config.HugePages, + }, + } + + data, err := json.MarshalIndent(c, "", " ") + if err != nil { + return err + } + return os.WriteFile(m.ConfigPath, data, 0644) +} diff --git a/pkg/mining/xmrig_stats.go b/pkg/mining/xmrig_stats.go new file mode 100644 index 0000000..27335a4 --- /dev/null +++ b/pkg/mining/xmrig_stats.go @@ -0,0 +1,49 @@ +package mining + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" +) + +// GetStats retrieves the performance statistics from the running XMRig miner. +func (m *XMRigMiner) GetStats() (*PerformanceMetrics, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + if !m.Running { + return nil, errors.New("miner is not running") + } + if m.API == nil || m.API.ListenPort == 0 { + return nil, errors.New("miner API not configured or port is zero") + } + + resp, err := httpClient.Get(fmt.Sprintf("http://%s:%d/2/summary", m.API.ListenHost, m.API.ListenPort)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to get stats: unexpected status code %d", resp.StatusCode) + } + + var summary XMRigSummary + if err := json.NewDecoder(resp.Body).Decode(&summary); err != nil { + return nil, err + } + + var hashrate int + if len(summary.Hashrate.Total) > 0 { + hashrate = int(summary.Hashrate.Total[0]) + } + + return &PerformanceMetrics{ + Hashrate: hashrate, + Shares: int(summary.Results.SharesGood), + Rejected: int(summary.Results.SharesTotal - summary.Results.SharesGood), + Uptime: int(summary.Uptime), + Algorithm: summary.Algorithm, + }, nil +} diff --git a/ui/src/app/app.css b/ui/src/app/app.css index 67b3b9b..fe9e4ea 100644 --- a/ui/src/app/app.css +++ b/ui/src/app/app.css @@ -1,79 +1,87 @@ :host { display: block; font-family: sans-serif; + width: 100%; } -.card { - max-width: 800px; - margin: 20px auto; +.mining-dashboard { + padding: 1rem; + width: 100%; + box-sizing: border-box; } -.header-container { +.centered-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + width: 100%; +} + +.card-overview, .card-error { + width: 100%; + margin-bottom: 1rem; + box-sizing: border-box; +} + +.card-header { display: flex; - flex-wrap: wrap; justify-content: space-between; align-items: center; - gap: 1rem; + width: 100%; } -.header-title { - margin: 0; - font-size: 1.25rem; - flex-grow: 1; +.card-error { + --base-color: var(--wa-color-red-200); + border-color: var(--wa-color-red-400); } -.title-quiet { - color: var(--wa-color-text-quiet); -} - -.header-stats { +.miner-list { display: flex; + flex-direction: column; gap: 1rem; +} + +.miner-item-container { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 0.75rem; + border-radius: var(--wa-border-radius-medium); + background-color: var(--wa-color-neutral-50); +} + +.miner-item { + display: flex; + justify-content: space-between; align-items: center; } -.stat-item { +.miner-name { + font-weight: bold; display: flex; align-items: center; gap: 0.5rem; - font-size: 0.9rem; } -.chart { - width: 100%; - height: 400px; - display: block; -} - -.details-section { - padding: 1rem; - background-color: var(--wa-color-neutral-50); - border-radius: var(--wa-border-radius-medium); -} - -.history-list { - list-style: none; - padding: 0; -} - -.card-footer { +.start-buttons { display: flex; - justify-content: flex-end; + gap: 0.5rem; } -.error-message { - color: var(--wa-color-danger-600); +.start-options { + display: flex; + flex-direction: column; + gap: 1rem; + border-top: 1px solid var(--wa-color-neutral-200); + padding-top: 1rem; } -/* Responsive stacking for smaller screens */ -@media (max-width: 600px) { - .header-container { - flex-direction: column; - align-items: flex-start; - } - - .header-stats { - width: 100%; - justify-content: space-between; - } +wa-button { + min-width: auto; +} + +wa-spinner { + margin: 1rem 0; } diff --git a/ui/src/app/app.html b/ui/src/app/app.html index 8bb5afa..a3606db 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -1,51 +1,127 @@ - +
-
-

- Mining Dashboard: {{ minerName }} -

-
-
- - {{ currentHashrate.toFixed(2) }} H/s -
-
- - {{ lastUpdated | date:'mediumTime' }} -
+ + @if (systemInfo === null) { +
+ +
Loading...
+ +
- - - -
+ } -