ax(mining): clarify CLI paths and usage examples
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

This commit is contained in:
Virgil 2026-04-04 07:20:56 +00:00
parent 9ed6c33c42
commit 6864e52ed4
8 changed files with 53 additions and 53 deletions

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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 <miner-type>",
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")
}

View file

@ -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.")

View file

@ -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)
}

View file

@ -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 {