adds historial hashrate data that reduces from 10s res to 1m res after a rolling 5m window.
This commit is contained in:
parent
6ac80d211a
commit
a043a09d22
7 changed files with 409 additions and 42 deletions
57
docs/docs.go
57
docs/docs.go
|
|
@ -145,6 +145,38 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/miners/{miner_name}/hashrate-history": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get historical hashrate data for a running miner",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"miners"
|
||||||
|
],
|
||||||
|
"summary": "Get miner hashrate history",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Miner Name",
|
||||||
|
"name": "miner_name",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/mining.HashratePoint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/miners/{miner_name}/stats": {
|
"/miners/{miner_name}/stats": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Get statistics for a running miner",
|
"description": "Get statistics for a running miner",
|
||||||
|
|
@ -531,6 +563,17 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mining.HashratePoint": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hashrate": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"mining.InstallationDetails": {
|
"mining.InstallationDetails": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -613,9 +656,23 @@ const docTemplate = `{
|
||||||
"configPath": {
|
"configPath": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"hashrateHistory": {
|
||||||
|
"description": "High-resolution (10s)",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/mining.HashratePoint"
|
||||||
|
}
|
||||||
|
},
|
||||||
"lastHeartbeat": {
|
"lastHeartbeat": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"lowResHashrateHistory": {
|
||||||
|
"description": "Low-resolution (1m averages)",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/mining.HashratePoint"
|
||||||
|
}
|
||||||
|
},
|
||||||
"miner_binary": {
|
"miner_binary": {
|
||||||
"description": "New field for the full path to the miner executable",
|
"description": "New field for the full path to the miner executable",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,38 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/miners/{miner_name}/hashrate-history": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get historical hashrate data for a running miner",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"miners"
|
||||||
|
],
|
||||||
|
"summary": "Get miner hashrate history",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Miner Name",
|
||||||
|
"name": "miner_name",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/mining.HashratePoint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/miners/{miner_name}/stats": {
|
"/miners/{miner_name}/stats": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Get statistics for a running miner",
|
"description": "Get statistics for a running miner",
|
||||||
|
|
@ -525,6 +557,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mining.HashratePoint": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hashrate": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"mining.InstallationDetails": {
|
"mining.InstallationDetails": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -607,9 +650,23 @@
|
||||||
"configPath": {
|
"configPath": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"hashrateHistory": {
|
||||||
|
"description": "High-resolution (10s)",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/mining.HashratePoint"
|
||||||
|
}
|
||||||
|
},
|
||||||
"lastHeartbeat": {
|
"lastHeartbeat": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"lowResHashrateHistory": {
|
||||||
|
"description": "Low-resolution (1m averages)",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/mining.HashratePoint"
|
||||||
|
}
|
||||||
|
},
|
||||||
"miner_binary": {
|
"miner_binary": {
|
||||||
"description": "New field for the full path to the miner executable",
|
"description": "New field for the full path to the miner executable",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,13 @@ definitions:
|
||||||
wallet:
|
wallet:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
mining.HashratePoint:
|
||||||
|
properties:
|
||||||
|
hashrate:
|
||||||
|
type: integer
|
||||||
|
timestamp:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
mining.InstallationDetails:
|
mining.InstallationDetails:
|
||||||
properties:
|
properties:
|
||||||
is_installed:
|
is_installed:
|
||||||
|
|
@ -207,8 +214,18 @@ definitions:
|
||||||
$ref: '#/definitions/mining.API'
|
$ref: '#/definitions/mining.API'
|
||||||
configPath:
|
configPath:
|
||||||
type: string
|
type: string
|
||||||
|
hashrateHistory:
|
||||||
|
description: High-resolution (10s)
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/mining.HashratePoint'
|
||||||
|
type: array
|
||||||
lastHeartbeat:
|
lastHeartbeat:
|
||||||
type: integer
|
type: integer
|
||||||
|
lowResHashrateHistory:
|
||||||
|
description: Low-resolution (1m averages)
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/mining.HashratePoint'
|
||||||
|
type: array
|
||||||
miner_binary:
|
miner_binary:
|
||||||
description: New field for the full path to the miner executable
|
description: New field for the full path to the miner executable
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -303,6 +320,27 @@ paths:
|
||||||
summary: Stop a running miner
|
summary: Stop a running miner
|
||||||
tags:
|
tags:
|
||||||
- miners
|
- miners
|
||||||
|
/miners/{miner_name}/hashrate-history:
|
||||||
|
get:
|
||||||
|
description: Get historical hashrate data for a running miner
|
||||||
|
parameters:
|
||||||
|
- description: Miner Name
|
||||||
|
in: path
|
||||||
|
name: miner_name
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/mining.HashratePoint'
|
||||||
|
type: array
|
||||||
|
summary: Get miner hashrate history
|
||||||
|
tags:
|
||||||
|
- miners
|
||||||
/miners/{miner_name}/stats:
|
/miners/{miner_name}/stats:
|
||||||
get:
|
get:
|
||||||
description: Get statistics for a running miner
|
description: Get statistics for a running miner
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,34 @@ package mining
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager handles miner lifecycle and operations
|
// Manager handles miner lifecycle and operations
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
miners map[string]Miner
|
miners map[string]Miner
|
||||||
|
mu sync.RWMutex // Mutex to protect the miners map
|
||||||
|
stopChan chan struct{}
|
||||||
|
waitGroup sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager creates a new miner manager
|
// NewManager creates a new miner manager
|
||||||
func NewManager() *Manager {
|
func NewManager() *Manager {
|
||||||
return &Manager{
|
m := &Manager{
|
||||||
miners: make(map[string]Miner),
|
miners: make(map[string]Miner),
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
waitGroup: sync.WaitGroup{},
|
||||||
}
|
}
|
||||||
|
m.startStatsCollection()
|
||||||
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartMiner starts a new miner with the given configuration
|
// StartMiner starts a new miner with the given configuration
|
||||||
func (m *Manager) StartMiner(minerType string, config *Config) (Miner, error) {
|
func (m *Manager) StartMiner(minerType string, config *Config) (Miner, error) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
var miner Miner
|
var miner Miner
|
||||||
switch strings.ToLower(minerType) {
|
switch strings.ToLower(minerType) {
|
||||||
case "xmrig":
|
case "xmrig":
|
||||||
|
|
@ -43,6 +55,9 @@ func (m *Manager) StartMiner(minerType string, config *Config) (Miner, error) {
|
||||||
|
|
||||||
// StopMiner stops a running miner
|
// StopMiner stops a running miner
|
||||||
func (m *Manager) StopMiner(name string) error {
|
func (m *Manager) StopMiner(name string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
minerKey := strings.ToLower(name) // Normalize input name to lowercase
|
minerKey := strings.ToLower(name) // Normalize input name to lowercase
|
||||||
miner, exists := m.miners[minerKey]
|
miner, exists := m.miners[minerKey]
|
||||||
if !exists {
|
if !exists {
|
||||||
|
|
@ -59,6 +74,9 @@ func (m *Manager) StopMiner(name string) error {
|
||||||
|
|
||||||
// GetMiner retrieves a miner by ID
|
// GetMiner retrieves a miner by ID
|
||||||
func (m *Manager) GetMiner(name string) (Miner, error) {
|
func (m *Manager) GetMiner(name string) (Miner, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
minerKey := strings.ToLower(name) // Normalize input name to lowercase
|
minerKey := strings.ToLower(name) // Normalize input name to lowercase
|
||||||
miner, exists := m.miners[minerKey]
|
miner, exists := m.miners[minerKey]
|
||||||
if !exists {
|
if !exists {
|
||||||
|
|
@ -69,6 +87,9 @@ func (m *Manager) GetMiner(name string) (Miner, error) {
|
||||||
|
|
||||||
// ListMiners returns all miners
|
// ListMiners returns all miners
|
||||||
func (m *Manager) ListMiners() []Miner {
|
func (m *Manager) ListMiners() []Miner {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
miners := make([]Miner, 0, len(m.miners))
|
miners := make([]Miner, 0, len(m.miners))
|
||||||
for _, miner := range m.miners {
|
for _, miner := range m.miners {
|
||||||
miners = append(miners, miner)
|
miners = append(miners, miner)
|
||||||
|
|
@ -85,3 +106,66 @@ func (m *Manager) ListAvailableMiners() []AvailableMiner {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// startStatsCollection starts a goroutine to periodically collect stats from active miners
|
||||||
|
func (m *Manager) startStatsCollection() {
|
||||||
|
m.waitGroup.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer m.waitGroup.Done()
|
||||||
|
ticker := time.NewTicker(HighResolutionInterval) // Collect stats every 10 seconds
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
m.collectMinerStats()
|
||||||
|
case <-m.stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectMinerStats iterates through active miners and collects their stats
|
||||||
|
func (m *Manager) collectMinerStats() {
|
||||||
|
m.mu.RLock()
|
||||||
|
minersToCollect := make([]Miner, 0, len(m.miners))
|
||||||
|
for _, miner := range m.miners {
|
||||||
|
minersToCollect = append(minersToCollect, miner)
|
||||||
|
}
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for _, miner := range minersToCollect {
|
||||||
|
stats, err := miner.GetStats()
|
||||||
|
if err != nil {
|
||||||
|
// Log the error but don't stop the collection for other miners
|
||||||
|
fmt.Printf("Error getting stats for miner %s: %v\n", miner.GetName(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
miner.AddHashratePoint(HashratePoint{
|
||||||
|
Timestamp: now,
|
||||||
|
Hashrate: stats.Hashrate,
|
||||||
|
})
|
||||||
|
miner.ReduceHashrateHistory(now) // Call the reducer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMinerHashrateHistory returns the hashrate history for a specific miner
|
||||||
|
func (m *Manager) GetMinerHashrateHistory(name string) ([]HashratePoint, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
minerKey := strings.ToLower(name)
|
||||||
|
miner, exists := m.miners[minerKey]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("miner not found: %s", name)
|
||||||
|
}
|
||||||
|
return miner.GetHashrateHistory(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the manager and its background goroutines
|
||||||
|
func (m *Manager) Stop() {
|
||||||
|
close(m.stopChan)
|
||||||
|
m.waitGroup.Wait() // Wait for the stats collection goroutine to finish
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,17 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// HighResolutionDuration is the duration for which hashrate data is kept at high resolution (10s intervals)
|
||||||
|
HighResolutionDuration = 5 * time.Minute
|
||||||
|
// HighResolutionInterval is the interval at which hashrate data is collected for high resolution
|
||||||
|
HighResolutionInterval = 10 * time.Second
|
||||||
|
// LowResolutionInterval is the interval for aggregated hashrate data (1m averages)
|
||||||
|
LowResolutionInterval = 1 * time.Minute
|
||||||
|
// LowResHistoryRetention is the duration for which low-resolution hashrate data is retained
|
||||||
|
LowResHistoryRetention = 24 * time.Hour // Example: keep 24 hours of 1-minute averages
|
||||||
|
)
|
||||||
|
|
||||||
// Miner is the interface for a miner
|
// Miner is the interface for a miner
|
||||||
type Miner interface {
|
type Miner interface {
|
||||||
Install() error
|
Install() error
|
||||||
|
|
@ -20,6 +31,9 @@ type Miner interface {
|
||||||
GetPath() string
|
GetPath() string
|
||||||
CheckInstallation() (*InstallationDetails, error)
|
CheckInstallation() (*InstallationDetails, error)
|
||||||
GetLatestVersion() (string, error)
|
GetLatestVersion() (string, error)
|
||||||
|
GetHashrateHistory() []HashratePoint // New method to get hashrate history
|
||||||
|
AddHashratePoint(point HashratePoint) // New method to add a hashrate point
|
||||||
|
ReduceHashrateHistory(now time.Time) // New method to trigger history reduction
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallationDetails contains information about an installed miner
|
// InstallationDetails contains information about an installed miner
|
||||||
|
|
@ -146,19 +160,28 @@ type History struct {
|
||||||
Updated int64 `json:"updated"`
|
Updated int64 `json:"updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HashratePoint represents a single hashrate measurement at a specific time
|
||||||
|
type HashratePoint struct {
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Hashrate int `json:"hashrate"`
|
||||||
|
}
|
||||||
|
|
||||||
// XMRigMiner represents an XMRig miner
|
// XMRigMiner represents an XMRig miner
|
||||||
type XMRigMiner struct {
|
type XMRigMiner struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Path string `json:"path"` // This will now be the versioned folder path
|
Path string `json:"path"` // This will now be the versioned folder path
|
||||||
MinerBinary string `json:"miner_binary"` // New field for the full path to the miner executable
|
MinerBinary string `json:"miner_binary"` // New field for the full path to the miner executable
|
||||||
Running bool `json:"running"`
|
Running bool `json:"running"`
|
||||||
LastHeartbeat int64 `json:"lastHeartbeat"`
|
LastHeartbeat int64 `json:"lastHeartbeat"`
|
||||||
ConfigPath string `json:"configPath"`
|
ConfigPath string `json:"configPath"`
|
||||||
API *API `json:"api"`
|
API *API `json:"api"`
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
cmd *exec.Cmd `json:"-"`
|
cmd *exec.Cmd `json:"-"`
|
||||||
|
HashrateHistory []HashratePoint `json:"hashrateHistory"` // High-resolution (10s)
|
||||||
|
LowResHashrateHistory []HashratePoint `json:"lowResHashrateHistory"` // Low-resolution (1m averages)
|
||||||
|
LastLowResAggregation time.Time `json:"-"` // Timestamp of the last low-res aggregation
|
||||||
}
|
}
|
||||||
|
|
||||||
// API represents the XMRig API configuration
|
// API represents the XMRig API configuration
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,9 @@ func (s *Service) ServiceStartup(ctx context.Context) error {
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
|
// Stop the manager's background goroutines
|
||||||
|
s.Manager.Stop()
|
||||||
|
|
||||||
ctxShutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctxShutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := s.Server.Shutdown(ctxShutdown); err != nil {
|
if err := s.Server.Shutdown(ctxShutdown); err != nil {
|
||||||
|
|
@ -88,6 +91,7 @@ func (s *Service) setupRoutes() {
|
||||||
minersGroup.DELETE("/:miner_name/uninstall", s.handleUninstallMiner)
|
minersGroup.DELETE("/:miner_name/uninstall", s.handleUninstallMiner)
|
||||||
minersGroup.DELETE("/:miner_name", s.handleStopMiner)
|
minersGroup.DELETE("/:miner_name", s.handleStopMiner)
|
||||||
minersGroup.GET("/:miner_name/stats", s.handleGetMinerStats)
|
minersGroup.GET("/:miner_name/stats", s.handleGetMinerStats)
|
||||||
|
minersGroup.GET("/:miner_name/hashrate-history", s.handleGetMinerHashrateHistory) // New endpoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -389,3 +393,21 @@ func (s *Service) handleGetMinerStats(c *gin.Context) {
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, stats)
|
c.JSON(http.StatusOK, stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleGetMinerHashrateHistory godoc
|
||||||
|
// @Summary Get miner hashrate history
|
||||||
|
// @Description Get historical hashrate data for a running miner
|
||||||
|
// @Tags miners
|
||||||
|
// @Produce json
|
||||||
|
// @Param miner_name path string true "Miner Name"
|
||||||
|
// @Success 200 {array} HashratePoint
|
||||||
|
// @Router /miners/{miner_name}/hashrate-history [get]
|
||||||
|
func (s *Service) handleGetMinerHashrateHistory(c *gin.Context) {
|
||||||
|
minerName := c.Param("miner_name")
|
||||||
|
history, err := s.Manager.GetMinerHashrateHistory(minerName)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, history)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -35,6 +36,9 @@ func NewXMRigMiner() *XMRigMiner {
|
||||||
ListenHost: "127.0.0.1",
|
ListenHost: "127.0.0.1",
|
||||||
ListenPort: 9000,
|
ListenPort: 9000,
|
||||||
},
|
},
|
||||||
|
HashrateHistory: make([]HashratePoint, 0),
|
||||||
|
LowResHashrateHistory: make([]HashratePoint, 0),
|
||||||
|
LastLowResAggregation: time.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -343,7 +347,6 @@ func (m *XMRigMiner) Start(config *Config) error {
|
||||||
if config.CPUNoYield {
|
if config.CPUNoYield {
|
||||||
args = append(args, "--cpu-no-yield")
|
args = append(args, "--cpu-no-yield")
|
||||||
}
|
}
|
||||||
// HugePages is handled by config file, but --no-huge-pages is a CLI option
|
|
||||||
if !config.HugePages { // If HugePages is explicitly false in config, add --no-huge-pages
|
if !config.HugePages { // If HugePages is explicitly false in config, add --no-huge-pages
|
||||||
args = append(args, "--no-huge-pages")
|
args = append(args, "--no-huge-pages")
|
||||||
}
|
}
|
||||||
|
|
@ -382,15 +385,13 @@ func (m *XMRigMiner) Start(config *Config) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// API options (CLI options override config file and m.API defaults)
|
// API options (CLI options override config file and m.API defaults)
|
||||||
// The API settings in m.API are used for GetStats, but CLI options can override for starting the miner
|
if m.API.Enabled {
|
||||||
if m.API.Enabled { // Only add API related CLI args if API is generally enabled
|
|
||||||
if config.APIWorkerID != "" {
|
if config.APIWorkerID != "" {
|
||||||
args = append(args, "--api-worker-id", config.APIWorkerID)
|
args = append(args, "--api-worker-id", config.APIWorkerID)
|
||||||
}
|
}
|
||||||
if config.APIID != "" {
|
if config.APIID != "" {
|
||||||
args = append(args, "--api-id", config.APIID)
|
args = append(args, "--api-id", config.APIID)
|
||||||
}
|
}
|
||||||
// Prefer config.HTTPHost/Port, fallback to m.API, then to XMRig defaults
|
|
||||||
if config.HTTPHost != "" {
|
if config.HTTPHost != "" {
|
||||||
args = append(args, "--http-host", config.HTTPHost)
|
args = append(args, "--http-host", config.HTTPHost)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -467,12 +468,10 @@ func (m *XMRigMiner) Start(config *Config) error {
|
||||||
args = append(args, "--no-dmi")
|
args = append(args, "--no-dmi")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print the command being executed for debugging
|
|
||||||
fmt.Fprintf(os.Stderr, "Executing XMRig command: %s %s\n", m.MinerBinary, strings.Join(args, " "))
|
fmt.Fprintf(os.Stderr, "Executing XMRig command: %s %s\n", m.MinerBinary, strings.Join(args, " "))
|
||||||
|
|
||||||
m.cmd = exec.Command(m.MinerBinary, args...)
|
m.cmd = exec.Command(m.MinerBinary, args...)
|
||||||
|
|
||||||
// If LogOutput is true, redirect stdout and stderr
|
|
||||||
if config.LogOutput {
|
if config.LogOutput {
|
||||||
m.cmd.Stdout = os.Stdout
|
m.cmd.Stdout = os.Stdout
|
||||||
m.cmd.Stderr = os.Stderr
|
m.cmd.Stderr = os.Stderr
|
||||||
|
|
@ -504,7 +503,6 @@ func (m *XMRigMiner) Stop() error {
|
||||||
return errors.New("miner is not running")
|
return errors.New("miner is not running")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kill the process. The goroutine in Start() will handle Wait() and state change.
|
|
||||||
return m.cmd.Process.Kill()
|
return m.cmd.Process.Kill()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -547,10 +545,118 @@ func (m *XMRigMiner) GetStats() (*PerformanceMetrics, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHashrateHistory returns the combined high-resolution and low-resolution hashrate history.
|
||||||
|
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.
|
||||||
|
func (m *XMRigMiner) AddHashratePoint(point HashratePoint) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
m.HashrateHistory = append(m.HashrateHistory, point)
|
||||||
|
// No trimming here; trimming is handled by ReduceHashrateHistory
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReduceHashrateHistory aggregates older high-resolution data into 1-minute averages
|
||||||
|
// and adds them to the low-resolution 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
|
||||||
|
// or if it's the first aggregation
|
||||||
|
if !m.LastLowResAggregation.IsZero() && now.Sub(m.LastLowResAggregation) < LowResolutionInterval {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find points in HashrateHistory that are older than HighResolutionDuration
|
||||||
|
// These are the candidates for aggregation into low-resolution history.
|
||||||
|
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.
|
||||||
|
// So, if HighResolutionDuration is 5 minutes, points older than (now - 5 minutes) are aggregated.
|
||||||
|
cutoff := now.Add(-HighResolutionDuration)
|
||||||
|
|
||||||
|
for _, p := range m.HashrateHistory {
|
||||||
|
if p.Timestamp.Before(cutoff) { // Use Before to ensure strict older-than
|
||||||
|
pointsToAggregate = append(pointsToAggregate, p)
|
||||||
|
} else {
|
||||||
|
newHighResHistory = append(newHighResHistory, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.HashrateHistory = newHighResHistory // Update high-res history to only contain recent points
|
||||||
|
|
||||||
|
if len(pointsToAggregate) == 0 {
|
||||||
|
// If no points to aggregate, just update LastLowResAggregation and return
|
||||||
|
m.LastLowResAggregation = now
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate into 1-minute slices
|
||||||
|
// Group points by minute (truncated timestamp)
|
||||||
|
minuteGroups := make(map[time.Time][]int)
|
||||||
|
for _, p := range pointsToAggregate {
|
||||||
|
// Round timestamp down to the nearest minute for grouping
|
||||||
|
minute := p.Timestamp.Truncate(LowResolutionInterval)
|
||||||
|
minuteGroups[minute] = append(minuteGroups[minute], p.Hashrate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate average for each minute and add to low-res history
|
||||||
|
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 new low-res points by timestamp to maintain chronological order
|
||||||
|
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)
|
||||||
|
// Find the first point that is *after* or equal to the lowResCutoff
|
||||||
|
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 { // All points are older than cutoff
|
||||||
|
firstValidLowResIndex = len(m.LowResHashrateHistory) // Clear all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.LowResHashrateHistory = m.LowResHashrateHistory[firstValidLowResIndex:]
|
||||||
|
|
||||||
|
m.LastLowResAggregation = now
|
||||||
|
}
|
||||||
|
|
||||||
func (m *XMRigMiner) createConfig(config *Config) error {
|
func (m *XMRigMiner) createConfig(config *Config) error {
|
||||||
configPath, err := xdg.ConfigFile("lethean-desktop/xmrig.json")
|
configPath, err := xdg.ConfigFile("lethean-desktop/xmrig.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Fallback to home directory if XDG is not available
|
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -563,7 +669,6 @@ func (m *XMRigMiner) createConfig(config *Config) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the config
|
|
||||||
c := map[string]interface{}{
|
c := map[string]interface{}{
|
||||||
"api": map[string]interface{}{
|
"api": map[string]interface{}{
|
||||||
"enabled": m.API.Enabled,
|
"enabled": m.API.Enabled,
|
||||||
|
|
@ -587,7 +692,6 @@ func (m *XMRigMiner) createConfig(config *Config) error {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the config to the file
|
|
||||||
data, err := json.MarshalIndent(c, "", " ")
|
data, err := json.MarshalIndent(c, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -603,21 +707,17 @@ func (m *XMRigMiner) unzip(src, dest string) error {
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
|
|
||||||
for _, f := range r.File {
|
for _, f := range r.File {
|
||||||
// Store filename/path for returning and using later on
|
|
||||||
fpath := filepath.Join(dest, f.Name)
|
fpath := filepath.Join(dest, f.Name)
|
||||||
|
|
||||||
// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
|
|
||||||
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
|
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
|
||||||
return fmt.Errorf("%s: illegal file path", fpath)
|
return fmt.Errorf("%s: illegal file path", fpath)
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.FileInfo().IsDir() {
|
if f.FileInfo().IsDir() {
|
||||||
// Make Folder
|
|
||||||
os.MkdirAll(fpath, os.ModePerm)
|
os.MkdirAll(fpath, os.ModePerm)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make File
|
|
||||||
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -634,7 +734,6 @@ func (m *XMRigMiner) unzip(src, dest string) error {
|
||||||
|
|
||||||
_, err = io.Copy(outFile, rc)
|
_, err = io.Copy(outFile, rc)
|
||||||
|
|
||||||
// Close the file without defer to close before next iteration of loop
|
|
||||||
outFile.Close()
|
outFile.Close()
|
||||||
rc.Close()
|
rc.Close()
|
||||||
|
|
||||||
|
|
@ -664,20 +763,14 @@ func (m *XMRigMiner) untar(src, dest string) error {
|
||||||
header, err := tr.Next()
|
header, err := tr.Next()
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
|
||||||
// if no more files are found return
|
|
||||||
case err == io.EOF:
|
case err == io.EOF:
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
// return any other error
|
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return err
|
return err
|
||||||
// if the header is nil, just skip it (not sure how this happens)
|
|
||||||
case header == nil:
|
case header == nil:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize the header name to prevent path traversal
|
|
||||||
cleanedName := filepath.Clean(header.Name)
|
cleanedName := filepath.Clean(header.Name)
|
||||||
if strings.HasPrefix(cleanedName, "..") || strings.HasPrefix(cleanedName, "/") || cleanedName == "." {
|
if strings.HasPrefix(cleanedName, "..") || strings.HasPrefix(cleanedName, "/") || cleanedName == "." {
|
||||||
continue
|
continue
|
||||||
|
|
@ -689,18 +782,13 @@ func (m *XMRigMiner) untar(src, dest string) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// check the file type
|
|
||||||
switch header.Typeflag {
|
switch header.Typeflag {
|
||||||
|
|
||||||
// if its a dir and it doesn't exist create it
|
|
||||||
case tar.TypeDir:
|
case tar.TypeDir:
|
||||||
if _, err := os.Stat(target); err != nil {
|
if _, err := os.Stat(target); err != nil {
|
||||||
if err := os.MkdirAll(target, 0755); err != nil {
|
if err := os.MkdirAll(target, 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it's a file create it
|
|
||||||
case tar.TypeReg:
|
case tar.TypeReg:
|
||||||
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -710,12 +798,10 @@ func (m *XMRigMiner) untar(src, dest string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy over contents
|
|
||||||
if _, err := io.Copy(f, tr); err != nil {
|
if _, err := io.Copy(f, tr); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// manually close here after each file operation; defering would cause each file to wait until all operations have completed.
|
|
||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue