feat: Update server configuration and add XMRig miner management functionality
This commit is contained in:
parent
6795a928d1
commit
2576d4bc1b
14 changed files with 1106 additions and 1415 deletions
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
15
docs/docs.go
15
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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
385
pkg/mining/miner.go
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
154
pkg/mining/xmrig_start.go
Normal 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
49
pkg/mining/xmrig_stats.go
Normal 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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue