From 6864e52ed4f1ca785c30ad1b1e7772950c3efb76 Mon Sep 17 00:00:00 2001 From: Virgil Date: Sat, 4 Apr 2026 07:20:56 +0000 Subject: [PATCH] ax(mining): clarify CLI paths and usage examples --- cmd/mining/cmd/doctor.go | 8 ++++---- cmd/mining/cmd/remote.go | 12 ++++++------ cmd/mining/cmd/serve.go | 14 +++++++------- cmd/mining/cmd/simulate.go | 12 ++++++------ cmd/mining/cmd/start.go | 10 +++++----- cmd/mining/cmd/update.go | 2 +- pkg/mining/repository.go | 12 ++++++------ pkg/mining/service.go | 36 ++++++++++++++++++------------------ 8 files changed, 53 insertions(+), 53 deletions(-) diff --git a/cmd/mining/cmd/doctor.go b/cmd/mining/cmd/doctor.go index a8a9aca..80bdadf 100644 --- a/cmd/mining/cmd/doctor.go +++ b/cmd/mining/cmd/doctor.go @@ -14,8 +14,8 @@ import ( ) const ( - installedMinersCachePointerFileName = ".installed-miners" - installedMinersCacheFileName = "installed-miners.json" + installedMinersPointerFileName = ".installed-miners" + installedMinersCacheFileName = "installed-miners.json" ) // validateInstalledMinerCachePath("/home/alice/.config/lethean-desktop/miners/installed-miners.json") returns nil. @@ -56,7 +56,7 @@ func loadAndDisplayInstalledMinerCache() (bool, error) { if err != nil { return false, fmt.Errorf("could not get home directory: %w", err) } - signpostPath := filepath.Join(homeDir, installedMinersCachePointerFileName) + signpostPath := filepath.Join(homeDir, installedMinersPointerFileName) // os.Stat("/home/alice/.installed-miners") returns os.ErrNotExist before the first `mining install xmrig` run. if _, err := os.Stat(signpostPath); os.IsNotExist(err) { @@ -135,7 +135,7 @@ func saveInstalledMinerCache(systemInfo *mining.SystemInfo) error { if err != nil { return fmt.Errorf("could not get home directory for signpost: %w", err) } - signpostPath := filepath.Join(homeDir, installedMinersCachePointerFileName) + signpostPath := filepath.Join(homeDir, installedMinersPointerFileName) if err := os.WriteFile(signpostPath, []byte(cacheFilePath), 0600); err != nil { return fmt.Errorf("could not write signpost file: %w", err) } diff --git a/cmd/mining/cmd/remote.go b/cmd/mining/cmd/remote.go index b462ec9..4a3790e 100644 --- a/cmd/mining/cmd/remote.go +++ b/cmd/mining/cmd/remote.go @@ -301,16 +301,16 @@ func init() { // remoteCmd.AddCommand(remoteStartCmd) // remote start peer-19f3 --type xmrig --profile default launches a miner. remoteCmd.AddCommand(remoteStartCmd) - remoteStartCmd.Flags().StringP("profile", "p", "", "Profile ID to use for starting the miner") - remoteStartCmd.Flags().StringP("type", "t", "", "Miner type, for example xmrig or tt-miner") + remoteStartCmd.Flags().StringP("profile", "p", "", "Profile ID to start, for example default or office-rig") + remoteStartCmd.Flags().StringP("type", "t", "", "Miner type to start, for example xmrig or tt-miner") // remoteCmd.AddCommand(remoteStopCmd) // remote stop peer-19f3 xmrig-main stops the selected miner. remoteCmd.AddCommand(remoteStopCmd) - remoteStopCmd.Flags().StringP("miner", "m", "", "Miner name to stop") + remoteStopCmd.Flags().StringP("miner", "m", "", "Miner name to stop, for example xmrig-main") // remoteCmd.AddCommand(remoteLogsCmd) // remote logs peer-19f3 xmrig-main prints miner logs. remoteCmd.AddCommand(remoteLogsCmd) - remoteLogsCmd.Flags().IntP("lines", "n", 100, "Number of log lines to retrieve") + remoteLogsCmd.Flags().IntP("lines", "n", 100, "Number of log lines to retrieve, for example 100") // remoteCmd.AddCommand(remoteConnectCmd) // remote connect peer-19f3 opens the peer connection. remoteCmd.AddCommand(remoteConnectCmd) @@ -320,7 +320,7 @@ func init() { // remoteCmd.AddCommand(remotePingCmd) // remote ping peer-19f3 --count 4 measures latency. remoteCmd.AddCommand(remotePingCmd) - remotePingCmd.Flags().IntP("count", "c", 4, "Number of pings to send") + remotePingCmd.Flags().IntP("count", "c", 4, "Number of ping samples to send, for example 4") } // getController() returns the cached controller after `node init` succeeds. @@ -368,7 +368,7 @@ func findPeerByPartialID(partialID string) *node.Peer { if strings.HasPrefix(peer.ID, partialID) { return peer } - // Also try matching by name + // strings.EqualFold(peer.Name, "office-rig") matches peers by display name as well as ID prefix. if strings.EqualFold(peer.Name, partialID) { return peer } diff --git a/cmd/mining/cmd/serve.go b/cmd/mining/cmd/serve.go index 87b01b2..19db38a 100644 --- a/cmd/mining/cmd/serve.go +++ b/cmd/mining/cmd/serve.go @@ -17,9 +17,9 @@ import ( ) var ( - serveHost string - servePort int - apiNamespace string + serveHost string + servePort int + apiBasePath string ) // mining serve starts the HTTP API and interactive shell. @@ -45,7 +45,7 @@ var serveCmd = &cobra.Command{ // serviceManager := getServiceManager() shares miner lifecycle state across `mining start`, `mining stop`, and `mining serve`. serviceManager := getServiceManager() - service, err := mining.NewService(serviceManager, listenAddress, displayAddress, apiNamespace) + service, err := mining.NewService(serviceManager, listenAddress, displayAddress, apiBasePath) if err != nil { return fmt.Errorf("failed to create new service: %w", err) } @@ -215,9 +215,9 @@ var serveCmd = &cobra.Command{ } func init() { - serveCmd.Flags().StringVar(&serveHost, "host", "127.0.0.1", "Host to listen on") - serveCmd.Flags().IntVarP(&servePort, "port", "p", 9090, "Port to listen on") - serveCmd.Flags().StringVarP(&apiNamespace, "namespace", "n", "/api/v1/mining", "API namespace for the swagger UI") + serveCmd.Flags().StringVar(&serveHost, "host", "127.0.0.1", "Host to bind the API server, for example 127.0.0.1 or 0.0.0.0") + serveCmd.Flags().IntVarP(&servePort, "port", "p", 9090, "Port to bind the API server, for example 9090") + serveCmd.Flags().StringVarP(&apiBasePath, "namespace", "n", "/api/v1/mining", "API base path, for example /api/v1/mining") rootCmd.AddCommand(serveCmd) } diff --git a/cmd/mining/cmd/simulate.go b/cmd/mining/cmd/simulate.go index 14eff5b..6480cb0 100644 --- a/cmd/mining/cmd/simulate.go +++ b/cmd/mining/cmd/simulate.go @@ -82,8 +82,8 @@ Available presets: simulatedConfig.Name, simulatedConfig.Algorithm, simulatedConfig.BaseHashrate) } - // service, err := mining.NewService(serviceManager, listenAddress, displayAddress, apiNamespace) // serves the simulator on http://127.0.0.1:9090. - service, err := mining.NewService(serviceManager, listenAddress, displayAddress, apiNamespace) + // service, err := mining.NewService(serviceManager, listenAddress, displayAddress, apiBasePath) // serves the simulator on http://127.0.0.1:9090. + service, err := mining.NewService(serviceManager, listenAddress, displayAddress, apiBasePath) if err != nil { return fmt.Errorf("failed to create new service: %w", err) } @@ -98,7 +98,7 @@ Available presets: fmt.Printf("\n=== SIMULATION MODE ===\n") fmt.Printf("Mining service started on http://%s:%d\n", displayHost, servePort) - fmt.Printf("Swagger documentation is available at http://%s:%d%s/swagger/index.html\n", displayHost, servePort, apiNamespace) + fmt.Printf("Swagger documentation is available at http://%s:%d%s/swagger/index.html\n", displayHost, servePort, apiBasePath) fmt.Printf("\nSimulating %d miner(s). Press Ctrl+C to stop.\n", simulatedMinerCount) fmt.Printf("Note: All data is simulated - no actual mining is occurring.\n\n") @@ -161,9 +161,9 @@ func init() { simulateCmd.Flags().IntVar(&simulationHashrate, "hashrate", 0, "Custom base hashrate (overrides preset)") simulateCmd.Flags().StringVar(&simulationAlgorithm, "algorithm", "", "Custom algorithm (overrides preset)") - simulateCmd.Flags().StringVar(&serveHost, "host", "127.0.0.1", "Host to listen on") - simulateCmd.Flags().IntVarP(&servePort, "port", "p", 9090, "Port to listen on") - simulateCmd.Flags().StringVarP(&apiNamespace, "namespace", "n", "/api/v1/mining", "API namespace") + simulateCmd.Flags().StringVar(&serveHost, "host", "127.0.0.1", "Host to bind the simulation API server, for example 127.0.0.1 or 0.0.0.0") + simulateCmd.Flags().IntVarP(&servePort, "port", "p", 9090, "Port to bind the simulation API server, for example 9090") + simulateCmd.Flags().StringVarP(&apiBasePath, "namespace", "n", "/api/v1/mining", "Simulation API base path, for example /api/v1/mining") rootCmd.AddCommand(simulateCmd) } diff --git a/cmd/mining/cmd/start.go b/cmd/mining/cmd/start.go index 700dafc..c20957a 100644 --- a/cmd/mining/cmd/start.go +++ b/cmd/mining/cmd/start.go @@ -13,11 +13,11 @@ var ( walletAddress string ) -// mining start xmrig --pool stratum+tcp://pool.example.com:3333 --wallet 44... starts a miner with explicit pool and wallet values. +// mining start xmrig --pool stratum+tcp://pool.example.com:3333 --wallet 44Affq5kSiGBoZ... starts a miner with explicit pool and wallet values. var startCmd = &cobra.Command{ - Use: "start [miner_name]", + Use: "start ", Short: "Start a new miner", - Long: `Start a new miner with the specified configuration.`, + Long: `Start a miner with an explicit pool URL and wallet address.`, Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { minerType := args[0] @@ -39,8 +39,8 @@ var startCmd = &cobra.Command{ func init() { rootCmd.AddCommand(startCmd) - startCmd.Flags().StringVarP(&poolAddress, "pool", "p", "", "Mining pool address (required)") - startCmd.Flags().StringVarP(&walletAddress, "wallet", "w", "", "Wallet address (required)") + startCmd.Flags().StringVarP(&poolAddress, "pool", "p", "", "Mining pool URL, for example stratum+tcp://pool.example.com:3333") + startCmd.Flags().StringVarP(&walletAddress, "wallet", "w", "", "Wallet address, for example 44Affq5kSiGBoZ...") _ = startCmd.MarkFlagRequired("pool") _ = startCmd.MarkFlagRequired("wallet") } diff --git a/cmd/mining/cmd/update.go b/cmd/mining/cmd/update.go index 9faf096..009b356 100644 --- a/cmd/mining/cmd/update.go +++ b/cmd/mining/cmd/update.go @@ -36,7 +36,7 @@ var updateCmd = &cobra.Command{ if err != nil { return fmt.Errorf("could not get home directory: %w", err) } - signpostPath := filepath.Join(homeDir, installedMinersCachePointerFileName) + signpostPath := filepath.Join(homeDir, installedMinersPointerFileName) if _, err := os.Stat(signpostPath); os.IsNotExist(err) { fmt.Println("No miners installed yet. Run 'doctor' or 'install' first.") diff --git a/pkg/mining/repository.go b/pkg/mining/repository.go index 3fdadd6..dce0dc4 100644 --- a/pkg/mining/repository.go +++ b/pkg/mining/repository.go @@ -10,13 +10,13 @@ import ( // repo := NewFileRepository[MinersConfig]("/home/alice/.config/lethean-desktop/miners.json") // data, err := repo.Load() type Repository[T any] interface { - // data, err := repo.Load() + // data, err := repo.Load() // loads "/home/alice/.config/lethean-desktop/miners.json" into MinersConfig Load() (T, error) - // repo.Save(updated) + // repo.Save(updatedConfig) persists the updated config back to "/home/alice/.config/lethean-desktop/miners.json" Save(data T) error - // repo.Update(func(d *T) error { d.Field = value; return nil }) + // repo.Update(func(configuration *MinersConfig) error { configuration.Miners = append(configuration.Miners, entry); return nil }) Update(modifier func(*T) error) error } @@ -106,7 +106,7 @@ func (repository *FileRepository[T]) Update(modifier func(*T) error) error { repository.mutex.Lock() defer repository.mutex.Unlock() - // Load current data + // os.ReadFile("/home/alice/.config/lethean-desktop/miners.json") loads the current config before applying the modifier. var data T fileData, err := os.ReadFile(repository.filePath) if err != nil { @@ -123,12 +123,12 @@ func (repository *FileRepository[T]) Update(modifier func(*T) error) error { } } - // Apply modification + // modifier(&data) can append `MinerAutostartConfig{MinerType: "xmrig", Autostart: true}` before saving. if err := modifier(&data); err != nil { return err } - // Save atomically + // repository.saveUnlocked(data) writes the updated JSON atomically to "/home/alice/.config/lethean-desktop/miners.json". return repository.saveUnlocked(data) } diff --git a/pkg/mining/service.go b/pkg/mining/service.go index ec1b195..d1cf13a 100644 --- a/pkg/mining/service.go +++ b/pkg/mining/service.go @@ -561,7 +561,7 @@ func (service *Service) ServiceStartup(ctx context.Context) error { go func() { <-ctx.Done() - service.Stop() // Clean up service resources (auth, event hub, node service) + service.Stop() // service.Stop() shuts down auth, the event hub, and node transport after `context.WithCancel(...)`. service.Manager.Stop() shutdownContext, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -570,7 +570,7 @@ func (service *Service) ServiceStartup(ctx context.Context) error { } }() - // Verify server is actually listening by attempting to connect + // net.DialTimeout("tcp", "127.0.0.1:9090", 50*time.Millisecond) confirms the listener is accepting connections before startup returns. maxRetries := 50 // 50 * 100ms = 5 seconds max for i := 0; i < maxRetries; i++ { select { @@ -580,7 +580,7 @@ func (service *Service) ServiceStartup(ctx context.Context) error { } return nil // Channel closed without error means server shut down default: - // Try to connect to verify server is listening + // net.DialTimeout("tcp", service.Server.Addr, 50*time.Millisecond) retries until the server is ready. conn, err := net.DialTimeout("tcp", service.Server.Addr, 50*time.Millisecond) if err == nil { conn.Close() @@ -598,11 +598,11 @@ func (service *Service) ServiceStartup(ctx context.Context) error { func (service *Service) SetupRoutes() { apiRoutes := service.Router.Group(service.APIBasePath) - // Health endpoints (no auth required for orchestration/monitoring) + // GET /api/v1/mining/health and GET /api/v1/mining/ready stay unauthenticated for health checks and process supervisors. apiRoutes.GET("/health", service.handleHealth) apiRoutes.GET("/ready", service.handleReady) - // Apply authentication middleware if enabled + // service.auth.Middleware() protects routes such as POST /api/v1/mining/doctor when API auth is enabled. if service.auth != nil { apiRoutes.Use(service.auth.Middleware()) } @@ -626,7 +626,7 @@ func (service *Service) SetupRoutes() { minersRoutes.POST("/:miner_name/stdin", service.handleMinerStdin) } - // Historical data endpoints (database-backed) + // GET /api/v1/mining/history/miners/xmrig-main serves database-backed hashrate history. historyRoutes := apiRoutes.Group("/history") { historyRoutes.GET("/status", service.handleHistoryStatus) @@ -645,13 +645,13 @@ func (service *Service) SetupRoutes() { profilesRoutes.POST("/:id/start", service.handleStartMinerWithProfile) } - // WebSocket endpoint for real-time events + // GET /api/v1/mining/ws/events upgrades clients to the real-time miner event stream. websocketRoutes := apiRoutes.Group("/ws") { websocketRoutes.GET("/events", service.handleWebSocketEvents) } - // Add P2P node endpoints if node service is available + // service.NodeService.SetupRoutes(apiRoutes) adds peer endpoints such as GET /api/v1/mining/node/peers when P2P is enabled. if service.NodeService != nil { service.NodeService.SetupRoutes(apiRoutes) } @@ -771,7 +771,7 @@ func (service *Service) handleGetInfo(requestContext *gin.Context) { // systemInfo, err := service.updateInstallationCache() // if err != nil { return ErrInternal("cache update failed").WithCause(err) } func (service *Service) updateInstallationCache() (*SystemInfo, error) { - // Always create a complete SystemInfo object + // &SystemInfo{InstalledMinersInfo: []*InstallationDetails{}} keeps GET /api/v1/mining/info stable even before any miners are installed. systemInfo := &SystemInfo{ Timestamp: time.Now(), OS: runtime.GOOS, @@ -789,7 +789,7 @@ func (service *Service) updateInstallationCache() (*SystemInfo, error) { for _, availableMiner := range service.Manager.ListAvailableMiners() { miner, err := CreateMiner(availableMiner.Name) if err != nil { - continue // Skip unsupported miner types + continue // CreateMiner("future-miner") failures are ignored so supported miners still appear in GET /api/v1/mining/info. } details, err := miner.CheckInstallation() if err != nil { @@ -798,21 +798,21 @@ func (service *Service) updateInstallationCache() (*SystemInfo, error) { systemInfo.InstalledMinersInfo = append(systemInfo.InstalledMinersInfo, details) } - configDir, err := xdg.ConfigFile("lethean-desktop/miners") + cacheDirectoryPath, err := xdg.ConfigFile("lethean-desktop/miners") if err != nil { return nil, ErrInternal("could not get config directory").WithCause(err) } - if err := os.MkdirAll(configDir, 0755); err != nil { + if err := os.MkdirAll(cacheDirectoryPath, 0755); err != nil { return nil, ErrInternal("could not create config directory").WithCause(err) } - configPath := filepath.Join(configDir, "config.json") + cacheFilePath := filepath.Join(cacheDirectoryPath, "config.json") data, err := json.MarshalIndent(systemInfo, "", " ") if err != nil { return nil, ErrInternal("could not marshal cache data").WithCause(err) } - if err := os.WriteFile(configPath, data, 0600); err != nil { + if err := os.WriteFile(cacheFilePath, data, 0600); err != nil { return nil, ErrInternal("could not write cache file").WithCause(err) } @@ -847,7 +847,7 @@ func (service *Service) handleUpdateCheck(requestContext *gin.Context) { for _, availableMiner := range service.Manager.ListAvailableMiners() { miner, err := CreateMiner(availableMiner.Name) if err != nil { - continue // Skip unsupported miner types + continue // CreateMiner("future-miner") failures are ignored so update checks still run for supported miners. } details, err := miner.CheckInstallation() @@ -983,7 +983,7 @@ func (service *Service) handleStartMinerWithProfile(requestContext *gin.Context) return } - // Validate config from profile to prevent shell injection and other issues + // minerConfig.Validate() rejects malformed pool URLs and wallet strings before the profile starts a miner process. if err := minerConfig.Validate(); err != nil { respondWithMiningError(requestContext, ErrInvalidConfig("profile config validation failed").WithCause(err)) return @@ -1327,7 +1327,7 @@ func (service *Service) handleMinerHistoricalHashrate(requestContext *gin.Contex return } - // Parse time range from query params, default to last 24 hours + // GET /api/v1/mining/history/miners/xmrig-main/hashrate?since=2026-04-03T00:00:00Z defaults to the last 24 hours when the query is empty. until := time.Now() since := until.Add(-24 * time.Hour) @@ -1366,7 +1366,7 @@ func (service *Service) handleWebSocketEvents(requestContext *gin.Context) { } logging.Info("new WebSocket connection", logging.Fields{"remote": requestContext.Request.RemoteAddr}) - // Only record connection after successful registration to avoid metrics race + // RecordWSConnection(true) runs only after EventHub accepts the socket, which keeps /metrics aligned with active clients. if service.EventHub.ServeWs(conn) { RecordWSConnection(true) } else {