From 0893b0ef9eeada7eb4dd08cc033efe830a981b70 Mon Sep 17 00:00:00 2001 From: Virgil Date: Sat, 4 Apr 2026 06:14:19 +0000 Subject: [PATCH] AX: clarify mining cache names and examples --- cmd/mining/cmd/doctor.go | 35 +++++++++++++++------------ cmd/mining/cmd/install.go | 2 +- cmd/mining/cmd/update.go | 9 ++++--- pkg/mining/manager.go | 51 ++++++++++++++++++++------------------- pkg/mining/service.go | 22 ++++++++--------- 5 files changed, 62 insertions(+), 57 deletions(-) diff --git a/cmd/mining/cmd/doctor.go b/cmd/mining/cmd/doctor.go index f188390..3f5be7d 100644 --- a/cmd/mining/cmd/doctor.go +++ b/cmd/mining/cmd/doctor.go @@ -13,11 +13,14 @@ import ( "github.com/spf13/cobra" ) -const installationCachePointerFileName = ".installed-miners" +const ( + installedMinersCachePointerFileName = ".installed-miners" + installedMinersCacheFileName = "installed-miners.json" +) -// validateCacheFilePath("/home/alice/.config/lethean-desktop/miners/config.json") returns nil. -// validateCacheFilePath("/tmp/config.json") rejects paths outside XDG_CONFIG_HOME. -func validateCacheFilePath(cacheFilePath string) error { +// validateInstalledMinerCachePath("/home/alice/.config/lethean-desktop/miners/installed-miners.json") returns nil. +// validateInstalledMinerCachePath("/tmp/installed-miners.json") rejects paths outside XDG_CONFIG_HOME. +func validateInstalledMinerCachePath(cacheFilePath string) error { expectedBase := filepath.Join(xdg.ConfigHome, "lethean-desktop") cleanPath := filepath.Clean(cacheFilePath) @@ -42,23 +45,23 @@ var doctorCmd = &cobra.Command{ if err := updateDoctorCache(); err != nil { return fmt.Errorf("failed to run doctor check: %w", err) } - // loadAndDisplayCache() prints the refreshed miner summary after `mining doctor` refreshes the cache. - _, err := loadAndDisplayCache() + // loadAndDisplayInstalledMinerCache() prints the refreshed miner summary after `mining doctor` refreshes the cache. + _, err := loadAndDisplayInstalledMinerCache() return err }, } -func loadAndDisplayCache() (bool, error) { +func loadAndDisplayInstalledMinerCache() (bool, error) { homeDir, err := os.UserHomeDir() if err != nil { return false, fmt.Errorf("could not get home directory: %w", err) } - signpostPath := filepath.Join(homeDir, installationCachePointerFileName) + signpostPath := filepath.Join(homeDir, installedMinersCachePointerFileName) // os.Stat("/home/alice/.installed-miners") returns os.ErrNotExist before the first `mining install xmrig` run. if _, err := os.Stat(signpostPath); os.IsNotExist(err) { fmt.Println("No cached data found. Run 'install' for a miner first.") - return false, nil // loadAndDisplayCache returns false until install writes the first cache file. + return false, nil // loadAndDisplayInstalledMinerCache returns false until install writes the first cache file. } cachePointerBytes, err := os.ReadFile(signpostPath) @@ -67,8 +70,8 @@ func loadAndDisplayCache() (bool, error) { } cacheFilePath := strings.TrimSpace(string(cachePointerBytes)) - // validateCacheFilePath("/home/alice/.config/lethean-desktop/miners/config.json") blocks path traversal outside XDG_CONFIG_HOME. - if err := validateCacheFilePath(cacheFilePath); err != nil { + // validateInstalledMinerCachePath("/home/alice/.config/lethean-desktop/miners/installed-miners.json") blocks path traversal outside XDG_CONFIG_HOME. + if err := validateInstalledMinerCachePath(cacheFilePath); err != nil { return false, fmt.Errorf("security error: %w", err) } @@ -102,13 +105,13 @@ func loadAndDisplayCache() (bool, error) { } else { minerName = "Unknown Miner" } - displayDetails(minerName, details) + displayInstalledMinerDetails(minerName, details) } return true, nil } -func saveResultsToCache(systemInfo *mining.SystemInfo) error { +func saveInstalledMinerCache(systemInfo *mining.SystemInfo) error { cacheDirectoryRelativePath := filepath.Join("lethean-desktop", "miners") cacheDirectoryPath, err := xdg.ConfigFile(cacheDirectoryRelativePath) if err != nil { @@ -117,7 +120,7 @@ func saveResultsToCache(systemInfo *mining.SystemInfo) error { if err := os.MkdirAll(cacheDirectoryPath, 0755); err != nil { return fmt.Errorf("could not create config directory: %w", err) } - cacheFilePath := filepath.Join(cacheDirectoryPath, "config.json") + cacheFilePath := filepath.Join(cacheDirectoryPath, installedMinersCacheFileName) data, err := json.MarshalIndent(systemInfo, "", " ") if err != nil { @@ -132,7 +135,7 @@ func saveResultsToCache(systemInfo *mining.SystemInfo) error { if err != nil { return fmt.Errorf("could not get home directory for signpost: %w", err) } - signpostPath := filepath.Join(homeDir, installationCachePointerFileName) + signpostPath := filepath.Join(homeDir, installedMinersCachePointerFileName) if err := os.WriteFile(signpostPath, []byte(cacheFilePath), 0600); err != nil { return fmt.Errorf("could not write signpost file: %w", err) } @@ -141,7 +144,7 @@ func saveResultsToCache(systemInfo *mining.SystemInfo) error { return nil } -func displayDetails(minerName string, details *mining.InstallationDetails) { +func displayInstalledMinerDetails(minerName string, details *mining.InstallationDetails) { fmt.Printf("--- %s ---\n", minerName) if details.IsInstalled { fmt.Printf(" Status: Installed\n") diff --git a/cmd/mining/cmd/install.go b/cmd/mining/cmd/install.go index 78a580e..d458c1c 100644 --- a/cmd/mining/cmd/install.go +++ b/cmd/mining/cmd/install.go @@ -96,7 +96,7 @@ func updateDoctorCache() error { InstalledMinersInfo: allDetails, } - return saveResultsToCache(systemInfo) + return saveInstalledMinerCache(systemInfo) } func init() { diff --git a/cmd/mining/cmd/update.go b/cmd/mining/cmd/update.go index f3d2710..a8bad6b 100644 --- a/cmd/mining/cmd/update.go +++ b/cmd/mining/cmd/update.go @@ -13,7 +13,8 @@ import ( "github.com/spf13/cobra" ) -// validateUpdateCacheFilePath("/home/alice/.config/lethean-desktop/miners/config.json") returns nil. +// validateUpdateCacheFilePath("/home/alice/.config/lethean-desktop/miners/installed-miners.json") returns nil. +// validateUpdateCacheFilePath("/tmp/installed-miners.json") rejects paths outside XDG_CONFIG_HOME. func validateUpdateCacheFilePath(cacheFilePath string) error { expectedBase := filepath.Join(xdg.ConfigHome, "lethean-desktop") cleanPath := filepath.Clean(cacheFilePath) @@ -35,7 +36,7 @@ var updateCmd = &cobra.Command{ if err != nil { return fmt.Errorf("could not get home directory: %w", err) } - signpostPath := filepath.Join(homeDir, installationCachePointerFileName) + signpostPath := filepath.Join(homeDir, installedMinersCachePointerFileName) if _, err := os.Stat(signpostPath); os.IsNotExist(err) { fmt.Println("No miners installed yet. Run 'doctor' or 'install' first.") @@ -48,7 +49,7 @@ var updateCmd = &cobra.Command{ } cacheFilePath := strings.TrimSpace(string(cachePointerBytes)) - // validateUpdateCacheFilePath("/home/alice/.config/lethean-desktop/miners/config.json") blocks path traversal. + // validateUpdateCacheFilePath("/home/alice/.config/lethean-desktop/miners/installed-miners.json") blocks path traversal outside XDG_CONFIG_HOME. if err := validateUpdateCacheFilePath(cacheFilePath); err != nil { return fmt.Errorf("security error: %w", err) } @@ -58,7 +59,7 @@ var updateCmd = &cobra.Command{ return fmt.Errorf("could not read cache file from %s: %w", cacheFilePath, err) } - // mining.SystemInfo{} matches the JSON shape that `mining doctor` writes to /home/alice/.config/lethean-desktop/miners/config.json. + // mining.SystemInfo{} matches the JSON shape that `mining doctor` writes to /home/alice/.config/lethean-desktop/miners/installed-miners.json. var systemInfo mining.SystemInfo if err := json.Unmarshal(cacheBytes, &systemInfo); err != nil { return fmt.Errorf("could not parse cache file: %w", err) diff --git a/pkg/mining/manager.go b/pkg/mining/manager.go index fb9a6c2..aa4db99 100644 --- a/pkg/mining/manager.go +++ b/pkg/mining/manager.go @@ -261,9 +261,11 @@ func findAvailablePort() (int, error) { return listener.Addr().(*net.TCPAddr).Port, nil } -// miner, err := manager.StartMiner(ctx, "xmrig", &Config{Algo: "rx/0"}) +// ctx, cancel := context.WithCancel(context.Background()) +// cancel() +// _, err := manager.StartMiner(ctx, "xmrig", &Config{Algo: "rx/0"}) // returns context.Canceled before locking func (manager *Manager) StartMiner(ctx context.Context, minerType string, config *Config) (Miner, error) { - // Check for cancellation before acquiring lock + // ctx, cancel := context.WithCancel(context.Background()); cancel(); manager.StartMiner(ctx, "xmrig", &Config{Algo: "rx/0"}) returns context.Canceled before locking. select { case <-ctx.Done(): return nil, ctx.Err() @@ -284,7 +286,7 @@ func (manager *Manager) StartMiner(ctx context.Context, minerType string, config instanceName := miner.GetName() if config.Algo != "" { - // Sanitize algo to prevent directory traversal or invalid filenames + // sanitizedAlgo := instanceNameRegex.ReplaceAllString("rx/0", "_") // "rx_0" sanitizedAlgo := instanceNameRegex.ReplaceAllString(config.Algo, "_") instanceName = instanceName + "-" + sanitizedAlgo } else { @@ -295,7 +297,7 @@ func (manager *Manager) StartMiner(ctx context.Context, minerType string, config return nil, ErrMinerExists(instanceName) } - // Validate user-provided HTTPPort if specified + // config.HTTPPort = 3333 keeps the miner API on a user-supplied port between 1024 and 65535. if config.HTTPPort != 0 { if config.HTTPPort < 1024 || config.HTTPPort > 65535 { return nil, ErrInvalidConfig("HTTPPort must be between 1024 and 65535, got " + strconv.Itoa(config.HTTPPort)) @@ -323,13 +325,13 @@ func (manager *Manager) StartMiner(ctx context.Context, minerType string, config } } - // manager.emitEvent(EventMinerStarting, MinerEventData{Name: "xmrig-rx_0"}) // fires before miner.Start(config) + // manager.emitEvent(EventMinerStarting, MinerEventData{Name: "xmrig-rx_0"}) fires before miner.Start(config). manager.emitEvent(EventMinerStarting, MinerEventData{ Name: instanceName, }) if err := miner.Start(config); err != nil { - // manager.emitEvent(EventMinerError, MinerEventData{Name: "xmrig-rx_0", Error: err.Error()}) + // manager.emitEvent(EventMinerError, MinerEventData{Name: "xmrig-rx_0", Error: err.Error()}) reports the failure before returning it. manager.emitEvent(EventMinerError, MinerEventData{ Name: instanceName, Error: err.Error(), @@ -346,7 +348,7 @@ func (manager *Manager) StartMiner(ctx context.Context, minerType string, config logMessage := "CryptoCurrency Miner started: " + miner.GetName() + " (Binary: " + miner.GetBinaryPath() + ")" logToSyslog(logMessage) - // Emit started event + // manager.emitEvent(EventMinerStarted, MinerEventData{Name: "xmrig-rx_0"}) marks the miner as running for websocket clients. manager.emitEvent(EventMinerStarted, MinerEventData{ Name: instanceName, }) @@ -355,10 +357,10 @@ func (manager *Manager) StartMiner(ctx context.Context, minerType string, config return miner, nil } -// manager.UninstallMiner(ctx, "xmrig") // stops all xmrig instances and removes config -// manager.UninstallMiner(ctx, "ttminer") // stops all ttminer instances and removes config +// manager.UninstallMiner(ctx, "xmrig") stops all xmrig instances and removes the matching config entry. +// manager.UninstallMiner(ctx, "ttminer") stops all ttminer instances and removes the matching config entry. func (manager *Manager) UninstallMiner(ctx context.Context, minerType string) error { - // Check for cancellation before acquiring lock + // ctx, cancel := context.WithCancel(context.Background()); cancel(); manager.UninstallMiner(ctx, "xmrig") returns context.Canceled before locking. select { case <-ctx.Done(): return ctx.Err() @@ -366,7 +368,7 @@ func (manager *Manager) UninstallMiner(ctx context.Context, minerType string) er } manager.mutex.Lock() - // Collect miners to stop and delete (can't modify map during iteration) + // manager.UninstallMiner(ctx, "xmrig") collects every running xmrig instance before removing it from the map. minersToDelete := make([]string, 0) minersToStop := make([]Miner, 0) for name, runningMiner := range manager.miners { @@ -379,13 +381,13 @@ func (manager *Manager) UninstallMiner(ctx context.Context, minerType string) er minersToDelete = append(minersToDelete, name) } } - // Delete from map first, then release lock before stopping (Stop may block) + // delete(manager.miners, "xmrig-rx_0") happens before stopping miners so Stop can block without holding the lock. for _, name := range minersToDelete { delete(manager.miners, name) } manager.mutex.Unlock() - // Stop miners outside the lock to avoid blocking + // miner.Stop() runs outside the lock so one slow uninstall does not block other manager calls. for i, miner := range minersToStop { if err := miner.Stop(); err != nil { logging.Warn("failed to stop running miner during uninstall", logging.Fields{"miner": minersToDelete[i], "error": err}) @@ -416,17 +418,17 @@ func (manager *Manager) UninstallMiner(ctx context.Context, minerType string) er // manager.updateMinerConfig("xmrig", true, config) // saves Autostart=true and the last-used config back to miners.json func (manager *Manager) updateMinerConfig(minerType string, autostart bool, config *Config) error { return UpdateMinersConfig(func(minersConfig *MinersConfig) error { - found := false + minerFound := false for i, minerConfig := range minersConfig.Miners { if equalFold(minerConfig.MinerType, minerType) { minersConfig.Miners[i].Autostart = autostart minersConfig.Miners[i].Config = config - found = true + minerFound = true break } } - if !found { + if !minerFound { minersConfig.Miners = append(minersConfig.Miners, MinerAutostartConfig{ MinerType: minerType, Autostart: autostart, @@ -437,10 +439,10 @@ func (manager *Manager) updateMinerConfig(minerType string, autostart bool, conf }) } -// manager.StopMiner(ctx, "xmrig/monero") -// manager.StopMiner(ctx, "ttminer/rtx4090") // still removes if already stopped +// manager.StopMiner(ctx, "xmrig/monero") stops the matching miner instance and removes it from the manager map. +// manager.StopMiner(ctx, "ttminer/rtx4090") still removes the entry when the miner has already stopped. func (manager *Manager) StopMiner(ctx context.Context, name string) error { - // Check for cancellation before acquiring lock + // ctx, cancel := context.WithCancel(context.Background()); cancel(); manager.StopMiner(ctx, "xmrig-rx_0") returns context.Canceled before locking. select { case <-ctx.Done(): return ctx.Err() @@ -466,19 +468,18 @@ func (manager *Manager) StopMiner(ctx context.Context, name string) error { return ErrMinerNotFound(name) } - // Emit stopping event + // manager.emitEvent(EventMinerStopping, MinerEventData{Name: "xmrig-rx_0"}) tells websocket clients shutdown has started. manager.emitEvent(EventMinerStopping, MinerEventData{ Name: name, }) - // Try to stop the miner, but always remove it from the map - // This handles the case where a miner crashed or was killed externally + // stopErr := miner.Stop() may fail after an external kill, but cleanup continues so the manager state stays accurate. stopErr := miner.Stop() - // Always remove from map - if it's not running, we still want to clean it up + // delete(manager.miners, "xmrig-rx_0") removes stale entries even when the process has already exited. delete(manager.miners, name) - // Emit stopped event + // manager.emitEvent(EventMinerStopped, MinerEventData{Name: "xmrig-rx_0", Reason: "stopped"}) confirms the final stop reason. reason := "stopped" if stopErr != nil && stopErr.Error() != "miner is not running" { reason = stopErr.Error() @@ -488,7 +489,7 @@ func (manager *Manager) StopMiner(ctx context.Context, name string) error { Reason: reason, }) - // Only return error if it wasn't just "miner is not running" + // stopErr = errors.New("permission denied") still returns the stop failure after the manager removes the stale entry. if stopErr != nil && stopErr.Error() != "miner is not running" { return stopErr } diff --git a/pkg/mining/service.go b/pkg/mining/service.go index 7d62042..9fd600d 100644 --- a/pkg/mining/service.go +++ b/pkg/mining/service.go @@ -198,23 +198,23 @@ func generateRequestID() string { return strconv.FormatInt(time.Now().UnixMilli(), 10) + "-" + hex.EncodeToString(randomBytes) } -// requestID := getRequestID(c) returns "trace-123" after requestIDMiddleware stores the incoming header. -func getRequestID(c *gin.Context) string { - if id, exists := c.Get("requestID"); exists { - if stringValue, ok := id.(string); ok { +// requestID := requestIDFromContext(c) returns "trace-123" after requestIDMiddleware stores the header. +func requestIDFromContext(c *gin.Context) string { + if requestIDValue, exists := c.Get("requestID"); exists { + if stringValue, ok := requestIDValue.(string); ok { return stringValue } } return "" } -// logWithRequestID(c, "error", "miner failed to start", logging.Fields{"type": "xmrig", "name": "xmrig-main"}) -// logWithRequestID(c, "info", "miner started", logging.Fields{"name": "xmrig-1", "request_id": "trace-123"}) -func logWithRequestID(c *gin.Context, level string, message string, fields logging.Fields) { +// logWithRequestContext(c, "error", "miner failed to start", logging.Fields{"type": "xmrig", "name": "xmrig-main"}) +// logWithRequestContext(c, "info", "miner started", logging.Fields{"name": "xmrig-1", "request_id": "trace-123"}) +func logWithRequestContext(c *gin.Context, level string, message string, fields logging.Fields) { if fields == nil { fields = logging.Fields{} } - if requestID := getRequestID(c); requestID != "" { + if requestID := requestIDFromContext(c); requestID != "" { fields["request_id"] = requestID } switch level { @@ -361,12 +361,12 @@ var wsUpgrader = websocket.Upgrader{ if origin == "" { return true // No Origin header, for example from curl or another non-browser client. } - // Parse the origin URL properly to prevent bypass attacks. - u, err := url.Parse(origin) + // parsedOrigin, err := url.Parse("http://localhost:4200") keeps browser-origin checks exact. + parsedOrigin, err := url.Parse(origin) if err != nil { return false } - host := u.Hostname() + host := parsedOrigin.Hostname() // Allow exact localhost matches like `http://127.0.0.1:4200`. return host == "localhost" || host == "127.0.0.1" || host == "::1" || host == "wails.localhost"