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": {
|
||||
"get": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -613,9 +656,23 @@ const docTemplate = `{
|
|||
"configPath": {
|
||||
"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"
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
"get": {
|
||||
"description": "Get statistics for a running miner",
|
||||
|
|
@ -525,6 +557,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"mining.HashratePoint": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hashrate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mining.InstallationDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -607,9 +650,23 @@
|
|||
"configPath": {
|
||||
"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"
|
||||
|
|
|
|||
|
|
@ -153,6 +153,13 @@ definitions:
|
|||
wallet:
|
||||
type: string
|
||||
type: object
|
||||
mining.HashratePoint:
|
||||
properties:
|
||||
hashrate:
|
||||
type: integer
|
||||
timestamp:
|
||||
type: string
|
||||
type: object
|
||||
mining.InstallationDetails:
|
||||
properties:
|
||||
is_installed:
|
||||
|
|
@ -207,8 +214,18 @@ definitions:
|
|||
$ref: '#/definitions/mining.API'
|
||||
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
|
||||
|
|
@ -303,6 +320,27 @@ paths:
|
|||
summary: Stop a running miner
|
||||
tags:
|
||||
- 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:
|
||||
get:
|
||||
description: Get statistics for a running miner
|
||||
|
|
|
|||
|
|
@ -3,22 +3,34 @@ package mining
|
|||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Manager handles miner lifecycle and operations
|
||||
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
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
miners: make(map[string]Miner),
|
||||
m := &Manager{
|
||||
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
|
||||
func (m *Manager) StartMiner(minerType string, config *Config) (Miner, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
var miner Miner
|
||||
switch strings.ToLower(minerType) {
|
||||
case "xmrig":
|
||||
|
|
@ -43,6 +55,9 @@ func (m *Manager) StartMiner(minerType string, config *Config) (Miner, error) {
|
|||
|
||||
// StopMiner stops a running miner
|
||||
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]
|
||||
if !exists {
|
||||
|
|
@ -59,6 +74,9 @@ func (m *Manager) StopMiner(name string) error {
|
|||
|
||||
// GetMiner retrieves a miner by ID
|
||||
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]
|
||||
if !exists {
|
||||
|
|
@ -69,6 +87,9 @@ func (m *Manager) GetMiner(name string) (Miner, error) {
|
|||
|
||||
// ListMiners returns all miners
|
||||
func (m *Manager) ListMiners() []Miner {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
miners := make([]Miner, 0, len(m.miners))
|
||||
for _, miner := range m.miners {
|
||||
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"
|
||||
)
|
||||
|
||||
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
|
||||
type Miner interface {
|
||||
Install() error
|
||||
|
|
@ -20,6 +31,9 @@ type Miner interface {
|
|||
GetPath() string
|
||||
CheckInstallation() (*InstallationDetails, 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
|
||||
|
|
@ -146,19 +160,28 @@ type History struct {
|
|||
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
|
||||
type XMRigMiner struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
URL string `json:"url"`
|
||||
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
|
||||
Running bool `json:"running"`
|
||||
LastHeartbeat int64 `json:"lastHeartbeat"`
|
||||
ConfigPath string `json:"configPath"`
|
||||
API *API `json:"api"`
|
||||
mu sync.Mutex
|
||||
cmd *exec.Cmd `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
URL string `json:"url"`
|
||||
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
|
||||
Running bool `json:"running"`
|
||||
LastHeartbeat int64 `json:"lastHeartbeat"`
|
||||
ConfigPath string `json:"configPath"`
|
||||
API *API `json:"api"`
|
||||
mu sync.Mutex
|
||||
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
|
||||
|
|
|
|||
|
|
@ -61,6 +61,9 @@ func (s *Service) ServiceStartup(ctx context.Context) error {
|
|||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
// Stop the manager's background goroutines
|
||||
s.Manager.Stop()
|
||||
|
||||
ctxShutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
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", s.handleStopMiner)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -35,6 +36,9 @@ func NewXMRigMiner() *XMRigMiner {
|
|||
ListenHost: "127.0.0.1",
|
||||
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 {
|
||||
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
|
||||
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)
|
||||
// The API settings in m.API are used for GetStats, but CLI options can override for starting the miner
|
||||
if m.API.Enabled { // Only add API related CLI args if API is generally enabled
|
||||
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)
|
||||
}
|
||||
// Prefer config.HTTPHost/Port, fallback to m.API, then to XMRig defaults
|
||||
if config.HTTPHost != "" {
|
||||
args = append(args, "--http-host", config.HTTPHost)
|
||||
} else {
|
||||
|
|
@ -467,12 +468,10 @@ func (m *XMRigMiner) Start(config *Config) error {
|
|||
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, " "))
|
||||
|
||||
m.cmd = exec.Command(m.MinerBinary, args...)
|
||||
|
||||
// If LogOutput is true, redirect stdout and stderr
|
||||
if config.LogOutput {
|
||||
m.cmd.Stdout = os.Stdout
|
||||
m.cmd.Stderr = os.Stderr
|
||||
|
|
@ -504,7 +503,6 @@ func (m *XMRigMiner) Stop() error {
|
|||
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()
|
||||
}
|
||||
|
||||
|
|
@ -547,10 +545,118 @@ func (m *XMRigMiner) GetStats() (*PerformanceMetrics, error) {
|
|||
}, 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 {
|
||||
configPath, err := xdg.ConfigFile("lethean-desktop/xmrig.json")
|
||||
if err != nil {
|
||||
// Fallback to home directory if XDG is not available
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -563,7 +669,6 @@ func (m *XMRigMiner) createConfig(config *Config) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Create the config
|
||||
c := map[string]interface{}{
|
||||
"api": map[string]interface{}{
|
||||
"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, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -603,21 +707,17 @@ func (m *XMRigMiner) unzip(src, dest string) error {
|
|||
defer r.Close()
|
||||
|
||||
for _, f := range r.File {
|
||||
// Store filename/path for returning and using later on
|
||||
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)) {
|
||||
return fmt.Errorf("%s: illegal file path", fpath)
|
||||
}
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
// Make Folder
|
||||
os.MkdirAll(fpath, os.ModePerm)
|
||||
continue
|
||||
}
|
||||
|
||||
// Make File
|
||||
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -634,7 +734,6 @@ func (m *XMRigMiner) unzip(src, dest string) error {
|
|||
|
||||
_, err = io.Copy(outFile, rc)
|
||||
|
||||
// Close the file without defer to close before next iteration of loop
|
||||
outFile.Close()
|
||||
rc.Close()
|
||||
|
||||
|
|
@ -664,20 +763,14 @@ func (m *XMRigMiner) untar(src, dest string) error {
|
|||
header, err := tr.Next()
|
||||
|
||||
switch {
|
||||
|
||||
// if no more files are found return
|
||||
case err == io.EOF:
|
||||
return nil
|
||||
|
||||
// return any other error
|
||||
case err != nil:
|
||||
return err
|
||||
// if the header is nil, just skip it (not sure how this happens)
|
||||
case header == nil:
|
||||
continue
|
||||
}
|
||||
|
||||
// Sanitize the header name to prevent path traversal
|
||||
cleanedName := filepath.Clean(header.Name)
|
||||
if strings.HasPrefix(cleanedName, "..") || strings.HasPrefix(cleanedName, "/") || cleanedName == "." {
|
||||
continue
|
||||
|
|
@ -689,18 +782,13 @@ func (m *XMRigMiner) untar(src, dest string) error {
|
|||
continue
|
||||
}
|
||||
|
||||
// check the file type
|
||||
switch header.Typeflag {
|
||||
|
||||
// if its a dir and it doesn't exist create it
|
||||
case tar.TypeDir:
|
||||
if _, err := os.Stat(target); err != nil {
|
||||
if err := os.MkdirAll(target, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// if it's a file create it
|
||||
case tar.TypeReg:
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
||||
return err
|
||||
|
|
@ -710,12 +798,10 @@ func (m *XMRigMiner) untar(src, dest string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// copy over contents
|
||||
if _, err := io.Copy(f, tr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// manually close here after each file operation; defering would cause each file to wait until all operations have completed.
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue