ax(mining): clarify CLI paths and usage examples
This commit is contained in:
parent
9ed6c33c42
commit
6864e52ed4
8 changed files with 53 additions and 53 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue