feat: Update server configuration and add XMRig miner management functionality

This commit is contained in:
Snider 2025-12-07 15:14:30 +00:00
parent 6795a928d1
commit 2576d4bc1b
14 changed files with 1106 additions and 1415 deletions

View file

@ -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)
}

View file

@ -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": {

View file

@ -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": {

View file

@ -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

View file

@ -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)
}

385
pkg/mining/miner.go Normal file
View file

@ -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()
}
}
}

View file

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

View file

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

View file

@ -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()
}
}
}

154
pkg/mining/xmrig_start.go Normal file
View file

@ -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)
}

49
pkg/mining/xmrig_stats.go Normal file
View file

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

View file

@ -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;
}

View file

@ -1,51 +1,127 @@
<wa-card class="card">
<div class="mining-dashboard">
<div slot="header" class="header-container">
<h3 class="header-title">
<span class="title-quiet">Mining Dashboard:</span> {{ minerName }}
</h3>
<div *ngIf="!loading && !error" class="header-stats">
<div class="stat-item">
<wa-icon name="speedometer"></wa-icon>
<span>{{ currentHashrate.toFixed(2) }} H/s</span>
</div>
<div class="stat-item">
<wa-icon name="clock"></wa-icon>
<span>{{ lastUpdated | date:'mediumTime' }}</span>
</div>
<!-- Initial Loading State -->
@if (systemInfo === null) {
<div class="centered-container">
<wa-card class="card-overview">
<div slot="header">Loading...</div>
<wa-spinner style="font-size: 3rem; margin-top: 1rem;"></wa-spinner>
</wa-card>
</div>
<wa-button appearance="plain" slot="header-actions" size="small" (click)="toggleDetails()">
<wa-icon name="cog" label="Toggle Details"></wa-icon>
</wa-button>
</div>
}
<div class="wa-stack wa-gap-sm">
<p *ngIf="loading">Loading hashrate data...</p>
<p *ngIf="error" class="error-message">Error: {{ error }}</p>
<div *ngIf="!loading && !error">
<highcharts-chart
[options]="chartOptions"
[(update)]="updateFlag"
class="chart"
></highcharts-chart>
<!-- API Not Available State -->
@if (systemInfo !== null && !apiAvailable) {
<div class="centered-container">
<wa-card class="card-overview">
<div slot="header">
<wa-icon name="cloud-off" style="font-size: 2rem;"></wa-icon>
API Not Available
</div>
<div>
<p>{{ error }}</p>
<p>Please ensure the mining API server is running and accessible.</p>
</div>
<div slot="footer">
<wa-button (click)="checkSystemState()">
<wa-icon name="arrow-clockwise" slot="prefix"></wa-icon>
Retry
</wa-button>
</div>
</wa-card>
</div>
}
<div *ngIf="showDetails" class="details-section">
<h4>Hashrate History ({{ hashrateHistory.length }} points)</h4>
<ul class="history-list">
<li *ngFor="let point of hashrateHistory | slice:0:5">
{{ point.timestamp | date:'mediumTime' }}: {{ point.hashrate }} H/s
</li>
<li *ngIf="hashrateHistory.length > 5">...</li>
</ul>
<h5>Raw History Data:</h5>
<pre>{{ hashrateHistory | json }}</pre>
<!-- Main Content -->
@if (systemInfo !== null && apiAvailable) {
<div>
@if (error) {
<wa-card class="card-error">
<div slot="header">
<wa-icon name="exclamation-triangle" style="font-size: 1.5rem;"></wa-icon>
An Error Occurred
</div>
<p>{{ error }}</p>
</wa-card>
}
<wa-card class="card-overview">
<div slot="header" class="card-header">
<div>
<wa-icon name="cpu" style="font-size: 1.5rem;"></wa-icon>
Mining Control
</div>
</div>
@if (installedMiners.length === 0) {
<div class="miner-list">
<div class="miner-item">
<span>No miners installed.</span>
</div>
@for (miner of availableMiners; track miner.name) {
<div class="miner-item">
<span>{{ miner.name }}</span>
<wa-button variant="success" (click)="installMiner(miner.name)">
<wa-icon name="download" slot="prefix"></wa-icon>
Install
</wa-button>
</div>
} @empty {
<div class="miner-item">
<span>Checking for available miners...</span>
</div>
}
</div>
}
@if (installedMiners.length > 0) {
<div class="miner-list">
@for (miner of installedMiners; track miner.path) {
<div class="miner-item-container">
<div class="miner-item">
<span class="miner-name">
{{ miner.type }}
<wa-tooltip>
<div slot="content">Version: {{ miner.version }}</div>
<wa-icon name="info-circle"></wa-icon>
</wa-tooltip>
</span>
@if (isMinerRunning(miner)) {
<wa-button variant="danger" (click)="stopMiner(miner)">
<wa-icon name="stop-circle" slot="prefix"></wa-icon>
Stop
</wa-button>
} @else {
<div class="start-buttons">
<wa-button variant="secondary" (click)="startMiner(miner, true)">
<wa-icon name="play-circle" slot="prefix"></wa-icon>
Start Last Config
</wa-button>
<wa-button variant="primary" (click)="toggleStartOptions(miner.type)">
<wa-icon name="gear" slot="prefix"></wa-icon>
New Config
</wa-button>
</div>
}
</div>
@if (showStartOptionsFor === miner.type) {
<div class="start-options">
<wa-input label="Pool Address" [(ngModel)]="poolAddress" name="poolAddress"></wa-input>
<wa-input label="Wallet Address" [(ngModel)]="walletAddress" name="walletAddress"></wa-input>
<wa-button variant="success" (click)="startMiner(miner)">
<wa-icon name="rocket-launch" slot="prefix"></wa-icon>
Confirm & Start
</wa-button>
</div>
}
</div>
}
</div>
}
</wa-card>
</div>
</div>
}
<wa-divider></wa-divider>
<div slot="footer" class="card-footer">
<wa-button variant="neutral" (click)="toggleDetails()">{{ showDetails ? 'Hide Details' : 'Show Details' }}</wa-button>
</div>
</wa-card>
</div>

View file

@ -1,147 +1,170 @@
import {
Component,
OnInit,
Input,
OnDestroy,
ElementRef,
ViewEncapsulation,
CUSTOM_ELEMENTS_SCHEMA
} from '@angular/core';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { HttpClient, HttpClientModule, HttpErrorResponse } from '@angular/common/http';
import { CommonModule } from '@angular/common';
import { interval, Subscription } from 'rxjs';
import { switchMap, startWith } from 'rxjs/operators';
import * as Highcharts from 'highcharts';
import { HighchartsChartComponent } from 'highcharts-angular';
import { FormsModule } from '@angular/forms';
import { of } from 'rxjs';
import { switchMap, catchError, map } from 'rxjs/operators';
// Import Shoelace components
// Import Web Awesome components
import "@awesome.me/webawesome/dist/webawesome.js";
import '@awesome.me/webawesome/dist/components/card/card.js';
import '@awesome.me/webawesome/dist/components/button/button.js';
import '@awesome.me/webawesome/dist/components/tooltip/tooltip.js';
import '@awesome.me/webawesome/dist/components/icon/icon.js';
interface HashratePoint {
timestamp: string; // ISO string
hashrate: number;
}
import '@awesome.me/webawesome/dist/components/spinner/spinner.js';
import '@awesome.me/webawesome/dist/components/input/input.js';
@Component({
selector: 'mde-mining-dashboard',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [CommonModule, HttpClientModule, HighchartsChartComponent],
imports: [CommonModule, HttpClientModule, FormsModule],
templateUrl: './app.html',
styleUrls: ["app.css"],
encapsulation: ViewEncapsulation.ShadowDom
})
export class MiningDashboardElementComponent implements OnInit, OnDestroy {
@Input() minerName: string = 'xmrig';
@Input() apiBaseUrl: string = 'http://localhost:9090/api/v1/mining';
export class MiningDashboardElementComponent implements OnInit {
apiBaseUrl: string = 'http://localhost:9090/api/v1/mining';
hashrateHistory: HashratePoint[] = [];
currentHashrate: number = 0;
lastUpdated: Date | null = null;
loading: boolean = true;
// State management
apiAvailable: boolean = true;
error: string | null = null;
showDetails: boolean = false;
private refreshSubscription: Subscription | undefined;
systemInfo: any = null;
availableMiners: any[] = [];
runningMiners: any[] = [];
installedMiners: any[] = [];
chartOptions: Highcharts.Options = {
chart: {
type: 'spline',
},
title: {
text: 'Live Hashrate'
},
xAxis: {
type: 'datetime',
title: {
text: 'Time'
}
},
yAxis: {
title: {
text: 'Hashrate (H/s)'
},
min: 0
},
series: [{
name: 'Hashrate',
type: 'line',
data: []
}],
credits: {
enabled: false
}
};
updateFlag = false;
// Form inputs
poolAddress: string = 'pool.hashvault.pro:80';
walletAddress: string = '888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H';
showStartOptionsFor: string | null = null;
constructor(private http: HttpClient, private elementRef: ElementRef) {}
ngOnInit(): void {
this.startAutoRefresh();
this.checkSystemState();
}
ngOnDestroy(): void {
this.stopAutoRefresh();
}
startAutoRefresh(): void {
this.stopAutoRefresh();
this.refreshSubscription = interval(10000)
.pipe(startWith(0), switchMap(() => this.fetchHashrateObservable()))
.subscribe({
next: (history) => {
this.hashrateHistory = history;
if (history && history.length > 0) {
this.currentHashrate = history[history.length - 1].hashrate;
this.lastUpdated = new Date(history[history.length - 1].timestamp);
const chartData = history.map(point => [
new Date(point.timestamp).getTime(),
point.hashrate
]);
// Safely update the chart data with type assertion
if (this.chartOptions.series && this.chartOptions.series[0]) {
(this.chartOptions.series[0] as Highcharts.SeriesLineOptions).data = chartData;
this.updateFlag = true; // Trigger chart update
}
} else {
this.currentHashrate = 0;
this.lastUpdated = null;
// Safely clear the chart data
if (this.chartOptions.series && this.chartOptions.series[0]) {
(this.chartOptions.series[0] as Highcharts.SeriesLineOptions).data = [];
this.updateFlag = true;
}
}
this.loading = false;
this.error = null;
},
error: (err) => {
console.error('Failed to fetch hashrate history:', err);
this.error = 'Failed to fetch hashrate history.';
this.loading = false;
}
});
}
stopAutoRefresh(): void {
if (this.refreshSubscription) {
this.refreshSubscription.unsubscribe();
this.refreshSubscription = undefined;
private handleError(err: HttpErrorResponse, defaultMessage: string) {
console.error(err);
if (err.error && err.error.error) {
// Handles { "error": "..." } from the backend
this.error = `${defaultMessage}: ${err.error.error}`;
} else if (typeof err.error === 'string' && err.error.length < 200) {
// Handles plain text errors
this.error = `${defaultMessage}: ${err.error}`;
} else {
this.error = `${defaultMessage}. Please check the console for details.`;
}
}
private fetchHashrateObservable() {
const url = `${this.apiBaseUrl}/miners/${this.minerName}/hashrate-history`;
return this.http.get<HashratePoint[]>(url);
checkSystemState() {
this.error = null;
this.http.get<any>(`${this.apiBaseUrl}/info`).pipe(
switchMap(info => {
this.apiAvailable = true;
this.systemInfo = info;
this.installedMiners = (info.installed_miners_info || [])
.filter((m: any) => m.is_installed)
.map((m: any) => ({ ...m, type: this.getMinerType(m) }));
if (this.installedMiners.length === 0) {
this.fetchAvailableMiners();
}
return this.fetchRunningMiners();
}),
catchError(err => {
this.apiAvailable = false;
this.error = 'Failed to connect to the mining API.';
this.systemInfo = {};
this.installedMiners = [];
this.runningMiners = [];
console.error('API not available:', err);
return of(null);
})
).subscribe();
}
toggleDetails(): void {
this.showDetails = !this.showDetails;
fetchAvailableMiners(): void {
this.http.get<any[]>(`${this.apiBaseUrl}/miners/available`).subscribe({
next: miners => { this.availableMiners = miners; },
error: err => { this.handleError(err, 'Could not fetch available miners'); }
});
}
fetchRunningMiners() {
return this.http.get<any[]>(`${this.apiBaseUrl}/miners`).pipe(
map(miners => { this.runningMiners = miners; }),
catchError(err => {
this.handleError(err, 'Could not fetch running miners');
this.runningMiners = [];
return of([]);
})
);
}
private performAction(action: any) {
action.subscribe({
next: () => {
setTimeout(() => this.checkSystemState(), 1000);
},
error: (err: HttpErrorResponse) => {
this.handleError(err, 'Action failed');
}
});
}
installMiner(minerType: string): void {
this.performAction(this.http.post(`${this.apiBaseUrl}/miners/${minerType}/install`, {}));
}
startMiner(miner: any, useLastConfig: boolean = false): void {
let config = {};
if (!useLastConfig) {
config = {
pool: this.poolAddress,
wallet: this.walletAddress,
tls: true,
hugePages: true,
};
}
this.performAction(this.http.post(`${this.apiBaseUrl}/miners/${miner.type}`, config));
this.showStartOptionsFor = null;
}
stopMiner(miner: any): void {
const runningInstance = this.getRunningMinerInstance(miner);
if (!runningInstance) {
this.error = "Cannot stop a miner that is not running.";
return;
}
this.performAction(this.http.delete(`${this.apiBaseUrl}/miners/${runningInstance.name}`));
}
toggleStartOptions(minerType: string): void {
this.showStartOptionsFor = this.showStartOptionsFor === minerType ? null : minerType;
}
getMinerType(miner: any): string {
if (!miner.path) return 'unknown';
const parts = miner.path.split('/').filter((p: string) => p);
return parts.length > 1 ? parts[parts.length - 2] : parts[parts.length - 1] || 'unknown';
}
getRunningMinerInstance(miner: any): any {
return this.runningMiners.find(m => m.name.startsWith(miner.type));
}
isMinerRunning(miner: any): boolean {
return !!this.getRunningMinerInstance(miner);
}
}