- Add CPUMaxThreadsHint, priority, pause-on-active/battery to XMRig config - Create SettingsManager for app preferences (window state, miner defaults) - Add settings API to desktop app service (GetSettings, SaveWindowState, etc) - Create throttle_test.go with multi-miner CPU usage verification tests - Create settings_manager_test.go with concurrent access tests - Desktop app now remembers window size between launches 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
386 lines
10 KiB
Go
386 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/Snider/Mining/pkg/mining"
|
|
"github.com/shirou/gopsutil/v4/cpu"
|
|
"github.com/shirou/gopsutil/v4/mem"
|
|
)
|
|
|
|
// MiningService exposes mining functionality to the Wails frontend.
|
|
type MiningService struct {
|
|
manager *mining.Manager
|
|
profileMgr *mining.ProfileManager
|
|
settingsMgr *mining.SettingsManager
|
|
}
|
|
|
|
// NewMiningService creates a new mining service with an initialized manager.
|
|
func NewMiningService() *MiningService {
|
|
manager := mining.NewManager()
|
|
profileMgr, _ := mining.NewProfileManager()
|
|
settingsMgr, _ := mining.NewSettingsManager()
|
|
return &MiningService{
|
|
manager: manager,
|
|
profileMgr: profileMgr,
|
|
settingsMgr: settingsMgr,
|
|
}
|
|
}
|
|
|
|
// SystemInfo represents system information for the frontend.
|
|
type SystemInfo struct {
|
|
Platform string `json:"platform"`
|
|
CPU string `json:"cpu"`
|
|
Cores int `json:"cores"`
|
|
MemoryGB int `json:"memory_gb"`
|
|
Miners []MinerInstallInfo `json:"installed_miners_info"`
|
|
}
|
|
|
|
// MinerInstallInfo represents installed miner information.
|
|
type MinerInstallInfo struct {
|
|
MinerType string `json:"miner_type"`
|
|
IsInstalled bool `json:"is_installed"`
|
|
Version string `json:"version"`
|
|
Path string `json:"path"`
|
|
}
|
|
|
|
// MinerStatus represents a running miner's status.
|
|
type MinerStatus struct {
|
|
Name string `json:"name"`
|
|
Running bool `json:"running"`
|
|
MinerType string `json:"miner_type"`
|
|
Stats *mining.PerformanceMetrics `json:"stats,omitempty"`
|
|
}
|
|
|
|
// Profile represents a mining profile for the frontend.
|
|
type Profile struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
MinerType string `json:"minerType"`
|
|
Config map[string]interface{} `json:"config"`
|
|
}
|
|
|
|
// GetSystemInfo returns system information and installed miners.
|
|
func (s *MiningService) GetSystemInfo() (*SystemInfo, error) {
|
|
cpuInfo, _ := cpu.Info()
|
|
cpuName := "Unknown"
|
|
if len(cpuInfo) > 0 {
|
|
cpuName = cpuInfo[0].ModelName
|
|
}
|
|
|
|
memInfo, _ := mem.VirtualMemory()
|
|
memGB := 0
|
|
if memInfo != nil {
|
|
memGB = int(memInfo.Total / 1024 / 1024 / 1024)
|
|
}
|
|
|
|
miners := []MinerInstallInfo{}
|
|
// Check installation for each miner type by creating temporary instances
|
|
for _, minerType := range []string{"xmrig", "tt-miner"} {
|
|
var miner mining.Miner
|
|
switch minerType {
|
|
case "xmrig":
|
|
miner = mining.NewXMRigMiner()
|
|
case "tt-miner":
|
|
miner = mining.NewTTMiner()
|
|
}
|
|
if miner != nil {
|
|
details, err := miner.CheckInstallation()
|
|
if err == nil && details != nil {
|
|
miners = append(miners, MinerInstallInfo{
|
|
MinerType: minerType,
|
|
IsInstalled: details.IsInstalled,
|
|
Version: details.Version,
|
|
Path: details.Path,
|
|
})
|
|
} else {
|
|
miners = append(miners, MinerInstallInfo{
|
|
MinerType: minerType,
|
|
IsInstalled: false,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return &SystemInfo{
|
|
Platform: runtime.GOOS,
|
|
CPU: cpuName,
|
|
Cores: runtime.NumCPU(),
|
|
MemoryGB: memGB,
|
|
Miners: miners,
|
|
}, nil
|
|
}
|
|
|
|
// ListMiners returns all running miners.
|
|
func (s *MiningService) ListMiners() []MinerStatus {
|
|
miners := s.manager.ListMiners()
|
|
result := make([]MinerStatus, len(miners))
|
|
for i, m := range miners {
|
|
stats, _ := m.GetStats()
|
|
result[i] = MinerStatus{
|
|
Name: m.GetName(),
|
|
Running: true, // If it's in the list, it's running
|
|
MinerType: getMinerType(m),
|
|
Stats: stats,
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// getMinerType extracts the miner type from a miner instance.
|
|
func getMinerType(m mining.Miner) string {
|
|
name := m.GetName()
|
|
if strings.HasPrefix(name, "xmrig") {
|
|
return "xmrig"
|
|
}
|
|
if strings.HasPrefix(name, "tt-miner") || strings.HasPrefix(name, "ttminer") {
|
|
return "tt-miner"
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
// StartMiner starts a miner with the given configuration.
|
|
func (s *MiningService) StartMiner(minerType string, config *mining.Config) (string, error) {
|
|
miner, err := s.manager.StartMiner(minerType, config)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return miner.GetName(), nil
|
|
}
|
|
|
|
// StartMinerFromProfile starts a miner using a saved profile.
|
|
func (s *MiningService) StartMinerFromProfile(profileID string) (string, error) {
|
|
if s.profileMgr == nil {
|
|
return "", fmt.Errorf("profile manager not initialized")
|
|
}
|
|
profile, ok := s.profileMgr.GetProfile(profileID)
|
|
if !ok {
|
|
return "", fmt.Errorf("profile not found: %s", profileID)
|
|
}
|
|
|
|
// Convert RawConfig to *Config
|
|
var config mining.Config
|
|
if profile.Config != nil {
|
|
if err := json.Unmarshal(profile.Config, &config); err != nil {
|
|
return "", fmt.Errorf("failed to parse profile config: %w", err)
|
|
}
|
|
}
|
|
|
|
miner, err := s.manager.StartMiner(profile.MinerType, &config)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return miner.GetName(), nil
|
|
}
|
|
|
|
// StopMiner stops a running miner by name.
|
|
func (s *MiningService) StopMiner(name string) error {
|
|
return s.manager.StopMiner(name)
|
|
}
|
|
|
|
// GetMinerStats returns stats for a specific miner.
|
|
func (s *MiningService) GetMinerStats(name string) (*mining.PerformanceMetrics, error) {
|
|
miner, err := s.manager.GetMiner(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return miner.GetStats()
|
|
}
|
|
|
|
// GetMinerLogs returns log lines for a specific miner.
|
|
func (s *MiningService) GetMinerLogs(name string) ([]string, error) {
|
|
miner, err := s.manager.GetMiner(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return miner.GetLogs(), nil
|
|
}
|
|
|
|
// InstallMiner installs a miner of the given type.
|
|
func (s *MiningService) InstallMiner(minerType string) error {
|
|
var miner mining.Miner
|
|
switch minerType {
|
|
case "xmrig":
|
|
miner = mining.NewXMRigMiner()
|
|
case "tt-miner":
|
|
miner = mining.NewTTMiner()
|
|
default:
|
|
return fmt.Errorf("unsupported miner type: %s", minerType)
|
|
}
|
|
return miner.Install()
|
|
}
|
|
|
|
// UninstallMiner uninstalls a miner of the given type.
|
|
func (s *MiningService) UninstallMiner(minerType string) error {
|
|
return s.manager.UninstallMiner(minerType)
|
|
}
|
|
|
|
// GetProfiles returns all saved mining profiles.
|
|
func (s *MiningService) GetProfiles() ([]Profile, error) {
|
|
if s.profileMgr == nil {
|
|
return []Profile{}, nil
|
|
}
|
|
profiles := s.profileMgr.GetAllProfiles()
|
|
|
|
result := make([]Profile, len(profiles))
|
|
for i, p := range profiles {
|
|
// Convert RawConfig to map for frontend
|
|
var configMap map[string]interface{}
|
|
if p.Config != nil {
|
|
json.Unmarshal(p.Config, &configMap)
|
|
}
|
|
result[i] = Profile{
|
|
ID: p.ID,
|
|
Name: p.Name,
|
|
MinerType: p.MinerType,
|
|
Config: configMap,
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// CreateProfile creates a new mining profile.
|
|
func (s *MiningService) CreateProfile(name, minerType string, config map[string]interface{}) (*Profile, error) {
|
|
if s.profileMgr == nil {
|
|
return nil, fmt.Errorf("profile manager not initialized")
|
|
}
|
|
|
|
// Convert map to RawConfig (JSON bytes)
|
|
configBytes, err := json.Marshal(config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal config: %w", err)
|
|
}
|
|
|
|
newProfile := &mining.MiningProfile{
|
|
Name: name,
|
|
MinerType: minerType,
|
|
Config: mining.RawConfig(configBytes),
|
|
}
|
|
|
|
profile, err := s.profileMgr.CreateProfile(newProfile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Profile{
|
|
ID: profile.ID,
|
|
Name: profile.Name,
|
|
MinerType: profile.MinerType,
|
|
Config: config,
|
|
}, nil
|
|
}
|
|
|
|
// DeleteProfile deletes a profile by ID.
|
|
func (s *MiningService) DeleteProfile(id string) error {
|
|
if s.profileMgr == nil {
|
|
return nil
|
|
}
|
|
return s.profileMgr.DeleteProfile(id)
|
|
}
|
|
|
|
// GetHashrateHistory returns hashrate history for a miner.
|
|
func (s *MiningService) GetHashrateHistory(name string) []mining.HashratePoint {
|
|
history, _ := s.manager.GetMinerHashrateHistory(name)
|
|
return history
|
|
}
|
|
|
|
// SendStdin sends input to a miner's stdin.
|
|
func (s *MiningService) SendStdin(name, input string) error {
|
|
miner, err := s.manager.GetMiner(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return miner.WriteStdin(input)
|
|
}
|
|
|
|
// Shutdown gracefully shuts down all miners.
|
|
func (s *MiningService) Shutdown() {
|
|
s.manager.Stop()
|
|
}
|
|
|
|
// === Settings Methods ===
|
|
|
|
// GetSettings returns the current app settings
|
|
func (s *MiningService) GetSettings() (*mining.AppSettings, error) {
|
|
if s.settingsMgr == nil {
|
|
return mining.DefaultSettings(), nil
|
|
}
|
|
return s.settingsMgr.Get(), nil
|
|
}
|
|
|
|
// SaveSettings saves the app settings
|
|
func (s *MiningService) SaveSettings(settings *mining.AppSettings) error {
|
|
if s.settingsMgr == nil {
|
|
return fmt.Errorf("settings manager not initialized")
|
|
}
|
|
return s.settingsMgr.Update(func(s *mining.AppSettings) {
|
|
*s = *settings
|
|
})
|
|
}
|
|
|
|
// SaveWindowState saves the window position and size
|
|
func (s *MiningService) SaveWindowState(x, y, width, height int, maximized bool) error {
|
|
if s.settingsMgr == nil {
|
|
return nil
|
|
}
|
|
return s.settingsMgr.UpdateWindowState(x, y, width, height, maximized)
|
|
}
|
|
|
|
// WindowState represents window position and size for the frontend
|
|
type WindowState struct {
|
|
X int `json:"x"`
|
|
Y int `json:"y"`
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
Maximized bool `json:"maximized"`
|
|
}
|
|
|
|
// GetWindowState returns the saved window state
|
|
func (s *MiningService) GetWindowState() *WindowState {
|
|
if s.settingsMgr == nil {
|
|
return &WindowState{Width: 1400, Height: 900}
|
|
}
|
|
state := s.settingsMgr.GetWindowState()
|
|
return &WindowState{
|
|
X: state.X,
|
|
Y: state.Y,
|
|
Width: state.Width,
|
|
Height: state.Height,
|
|
Maximized: state.Maximized,
|
|
}
|
|
}
|
|
|
|
// SetStartOnBoot enables/disables start on system boot
|
|
func (s *MiningService) SetStartOnBoot(enabled bool) error {
|
|
if s.settingsMgr == nil {
|
|
return nil
|
|
}
|
|
return s.settingsMgr.SetStartOnBoot(enabled)
|
|
}
|
|
|
|
// SetAutostartMiners enables/disables automatic miner start
|
|
func (s *MiningService) SetAutostartMiners(enabled bool) error {
|
|
if s.settingsMgr == nil {
|
|
return nil
|
|
}
|
|
return s.settingsMgr.SetAutostartMiners(enabled)
|
|
}
|
|
|
|
// SetCPUThrottle configures CPU throttling settings
|
|
func (s *MiningService) SetCPUThrottle(enabled bool, maxPercent int) error {
|
|
if s.settingsMgr == nil {
|
|
return nil
|
|
}
|
|
return s.settingsMgr.SetCPUThrottle(enabled, maxPercent)
|
|
}
|
|
|
|
// SetMinerDefaults updates default miner configuration
|
|
func (s *MiningService) SetMinerDefaults(defaults mining.MinerDefaults) error {
|
|
if s.settingsMgr == nil {
|
|
return nil
|
|
}
|
|
return s.settingsMgr.SetMinerDefaults(defaults)
|
|
}
|