Added tests for StartMiner and StopMiner in the manager, increasing test coverage for the pkg/mining package. Refactored the findMinerBinary function to fall back to the system PATH, making the application more robust and easier to test.
826 lines
22 KiB
Go
826 lines
22 KiB
Go
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"
|
|
)
|
|
|
|
var httpClient = &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
// NewXMRigMiner creates a new XMRig miner
|
|
func NewXMRigMiner() *XMRigMiner {
|
|
return &XMRigMiner{
|
|
Name: "xmrig", // Changed to lowercase for consistency
|
|
Version: "latest",
|
|
URL: "https://github.com/xmrig/xmrig/releases",
|
|
API: &API{
|
|
Enabled: true,
|
|
ListenHost: "127.0.0.1",
|
|
ListenPort: 9000,
|
|
},
|
|
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 path of the miner
|
|
// This now returns the base installation directory for xmrig, not the versioned one.
|
|
func (m *XMRigMiner) GetPath() string {
|
|
dataPath, err := xdg.DataFile("lethean-desktop/miners/xmrig")
|
|
if err != nil {
|
|
// Fallback for safety, though it should ideally not fail if Install works.
|
|
return ""
|
|
}
|
|
return dataPath
|
|
}
|
|
|
|
// GetBinaryPath returns the full path to the miner executable.
|
|
func (m *XMRigMiner) GetBinaryPath() string {
|
|
return m.MinerBinary
|
|
}
|
|
|
|
// GetLatestVersion returns the latest version of XMRig
|
|
func (m *XMRigMiner) GetLatestVersion() (string, error) {
|
|
resp, err := httpClient.Get("https://api.github.com/repos/xmrig/xmrig/releases/latest")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("failed to get latest release: unexpected status code %d", resp.StatusCode)
|
|
}
|
|
|
|
var release struct {
|
|
TagName string `json:"tag_name"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
|
return "", err
|
|
}
|
|
return release.TagName, nil
|
|
}
|
|
|
|
// Download and install the latest version of XMRig
|
|
func (m *XMRigMiner) Install() error {
|
|
version, err := m.GetLatestVersion()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.Version = version
|
|
|
|
// Construct the download URL
|
|
var url string
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
url = fmt.Sprintf("https://github.com/xmrig/xmrig/releases/download/%s/xmrig-%s-msvc-win64.zip", version, strings.TrimPrefix(version, "v"))
|
|
case "linux":
|
|
url = fmt.Sprintf("https://github.com/xmrig/xmrig/releases/download/%s/xmrig-%s-linux-x64.tar.gz", version, strings.TrimPrefix(version, "v"))
|
|
case "darwin":
|
|
url = fmt.Sprintf("https://github.com/xmrig/xmrig/releases/download/%s/xmrig-%s-macos-x64.tar.gz", version, strings.TrimPrefix(version, "v"))
|
|
default:
|
|
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 {
|
|
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
|
|
_, err = m.CheckInstallation()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to verify installation after extraction: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Uninstall removes the miner files
|
|
func (m *XMRigMiner) Uninstall() error {
|
|
// Uninstall should remove the base path, which contains the versioned folder
|
|
return os.RemoveAll(m.GetPath())
|
|
}
|
|
|
|
// findMinerBinary searches for the miner executable, first in the standard installation path,
|
|
// then falls back to the system's PATH.
|
|
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 checks if the miner is installed and returns its 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 the miner
|
|
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 the miner
|
|
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 returns the stats for the miner
|
|
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.
|
|
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) GetHighResHistoryLength() int {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return len(m.HashrateHistory)
|
|
}
|
|
|
|
func (m *XMRigMiner) GetLowResHistoryLength() int {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return len(m.LowResHashrateHistory)
|
|
}
|
|
|
|
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 {
|
|
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)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
}
|