From a675d16ed6deb2daa7e41a70aa5ab99a48e5409b Mon Sep 17 00:00:00 2001 From: Virgil Date: Sat, 4 Apr 2026 05:39:20 +0000 Subject: [PATCH] AX: clarify mining service names and comments --- cmd/mining/cmd/serve.go | 38 ++++++++-------- cmd/mining/cmd/simulate.go | 6 +-- cmd/mining/cmd/start.go | 4 +- cmd/mining/cmd/status.go | 4 +- pkg/mining/service.go | 88 ++++++++++++++++++-------------------- 5 files changed, 67 insertions(+), 73 deletions(-) diff --git a/cmd/mining/cmd/serve.go b/cmd/mining/cmd/serve.go index 26ac2f5..7f21b2e 100644 --- a/cmd/mining/cmd/serve.go +++ b/cmd/mining/cmd/serve.go @@ -31,28 +31,28 @@ var serveCmd = &cobra.Command{ ctx, cancel := context.WithCancel(context.Background()) defer cancel() - displayHost := host - if displayHost == "0.0.0.0" { + displayHostName := host + if displayHostName == "0.0.0.0" { var err error - displayHost, err = getLocalIP() + displayHostName, err = getLocalIP() if err != nil { - displayHost = "localhost" + displayHostName = "localhost" } } - displayAddr := fmt.Sprintf("%s:%d", displayHost, port) - listenAddr := fmt.Sprintf("%s:%d", host, port) + displayAddress := fmt.Sprintf("%s:%d", displayHostName, port) + listenAddress := fmt.Sprintf("%s:%d", host, port) - // miningManager := getServiceManager() shares the same miner lifecycle state across CLI commands. - miningManager := getServiceManager() + // manager := getServiceManager() shares the same miner lifecycle state across CLI commands. + manager := getServiceManager() - miningService, err := mining.NewService(miningManager, listenAddr, displayAddr, namespace) + service, err := mining.NewService(manager, listenAddress, displayAddress, namespace) if err != nil { return fmt.Errorf("failed to create new service: %w", err) } - // miningService.ServiceStartup(ctx) starts the HTTP server while the shell keeps reading stdin. + // service.ServiceStartup(ctx) starts the HTTP server while the shell keeps reading stdin. go func() { - if err := miningService.ServiceStartup(ctx); err != nil { + if err := service.ServiceStartup(ctx); err != nil { fmt.Fprintf(os.Stderr, "Failed to start service: %v\n", err) cancel() } @@ -64,8 +64,8 @@ var serveCmd = &cobra.Command{ // go func() { fmt.Print(">> ") } // keeps the interactive shell responsive while the API serves requests. go func() { - fmt.Printf("Mining service started on http://%s:%d\n", displayHost, port) - fmt.Printf("Swagger documentation is available at http://%s:%d%s/index.html\n", displayHost, port, miningService.SwaggerUIPath) + fmt.Printf("Mining service started on http://%s:%d\n", displayHostName, port) + fmt.Printf("Swagger documentation is available at http://%s:%d%s/index.html\n", displayHostName, port, service.SwaggerUIPath) fmt.Println("Entering interactive shell. Type 'exit' or 'quit' to stop.") fmt.Print(">> ") @@ -135,7 +135,7 @@ var serveCmd = &cobra.Command{ continue } - miner, err := miningManager.StartMiner(context.Background(), minerType, config) + miner, err := manager.StartMiner(context.Background(), minerType, config) if err != nil { fmt.Fprintf(os.Stderr, "Error starting miner: %v\n", err) } else { @@ -147,7 +147,7 @@ var serveCmd = &cobra.Command{ fmt.Println("Error: status command requires miner name, for example `status xmrig`") } else { minerName := commandArgs[0] - miner, err := miningManager.GetMiner(minerName) + miner, err := manager.GetMiner(minerName) if err != nil { fmt.Fprintf(os.Stderr, "Error getting miner status: %v\n", err) } else { @@ -169,7 +169,7 @@ var serveCmd = &cobra.Command{ fmt.Println("Error: stop command requires miner name, for example `stop xmrig`") } else { minerName := commandArgs[0] - err := miningManager.StopMiner(context.Background(), minerName) + err := manager.StopMiner(context.Background(), minerName) if err != nil { fmt.Fprintf(os.Stderr, "Error stopping miner: %v\n", err) } else { @@ -177,7 +177,7 @@ var serveCmd = &cobra.Command{ } } case "list": - miners := miningManager.ListMiners() + miners := manager.ListMiners() if len(miners) == 0 { fmt.Println("No miners currently running.") } else { @@ -206,8 +206,8 @@ var serveCmd = &cobra.Command{ case <-ctx.Done(): } - // miningManager.Stop() stops miner goroutines and closes the shared manager before exit. - miningManager.Stop() + // manager.Stop() stops miner goroutines and closes the shared manager before exit. + manager.Stop() fmt.Println("Mining service stopped.") return nil diff --git a/cmd/mining/cmd/simulate.go b/cmd/mining/cmd/simulate.go index 9dedb5b..bc7fb3f 100644 --- a/cmd/mining/cmd/simulate.go +++ b/cmd/mining/cmd/simulate.go @@ -57,8 +57,8 @@ Available presets: displayHost = "localhost" } } - displayAddr := fmt.Sprintf("%s:%d", displayHost, port) - listenAddr := fmt.Sprintf("%s:%d", host, port) + displayAddress := fmt.Sprintf("%s:%d", displayHost, port) + listenAddress := fmt.Sprintf("%s:%d", host, port) // manager := mining.NewManagerForSimulation() // keeps simulated miners isolated from the real autostart state. manager := mining.NewManagerForSimulation() @@ -83,7 +83,7 @@ Available presets: } // service, err := mining.NewService(manager, listenAddr, displayAddr, namespace) // serves the simulator on http://127.0.0.1:9090. - service, err := mining.NewService(manager, listenAddr, displayAddr, namespace) + service, err := mining.NewService(manager, listenAddress, displayAddress, namespace) if err != nil { return fmt.Errorf("failed to create new service: %w", err) } diff --git a/cmd/mining/cmd/start.go b/cmd/mining/cmd/start.go index c6418ec..98a4e8c 100644 --- a/cmd/mining/cmd/start.go +++ b/cmd/mining/cmd/start.go @@ -20,13 +20,13 @@ var startCmd = &cobra.Command{ Long: `Start a new miner with the specified configuration.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - selectedMinerType := args[0] + minerType := args[0] config := &mining.Config{ Pool: poolAddress, Wallet: walletAddress, } - miner, err := getServiceManager().StartMiner(context.Background(), selectedMinerType, config) + miner, err := getServiceManager().StartMiner(context.Background(), minerType, config) if err != nil { return fmt.Errorf("failed to start miner: %w", err) } diff --git a/cmd/mining/cmd/status.go b/cmd/mining/cmd/status.go index 8f654d0..08d0ea6 100644 --- a/cmd/mining/cmd/status.go +++ b/cmd/mining/cmd/status.go @@ -17,9 +17,9 @@ var statusCmd = &cobra.Command{ Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { minerName := args[0] - miningManager := getServiceManager() + manager := getServiceManager() - miner, err := miningManager.GetMiner(minerName) + miner, err := manager.GetMiner(minerName) if err != nil { return fmt.Errorf("failed to get miner: %w", err) } diff --git a/pkg/mining/service.go b/pkg/mining/service.go index e49be53..a93f7a1 100644 --- a/pkg/mining/service.go +++ b/pkg/mining/service.go @@ -60,10 +60,10 @@ type APIError struct { Retryable bool `json:"retryable"` // Can the client retry? } -// if debugErrorsEnabled { /* details exposed in API responses */ } // true when DEBUG_ERRORS=true or GIN_MODE != "release" +// debugErrorsEnabled is true when DEBUG_ERRORS=true or GIN_MODE != "release". var debugErrorsEnabled = os.Getenv("DEBUG_ERRORS") == "true" || os.Getenv("GIN_MODE") != "release" -// sanitizeErrorDetails("exec: file not found") // => "" in production, => "exec: file not found" in debug mode +// sanitizeErrorDetails("exec: file not found") returns "" in production and the full string in debug mode. func sanitizeErrorDetails(details string) string { if debugErrorsEnabled { return details @@ -71,7 +71,7 @@ func sanitizeErrorDetails(details string) string { return "" } -// respondWithError(c, http.StatusNotFound, ErrCodeMinerNotFound, "xmrig not found", err.Error()) +// respondWithError(c, http.StatusNotFound, ErrCodeMinerNotFound, "xmrig not found", "process exited with code 1") func respondWithError(c *gin.Context, status int, code string, message string, details string) { apiError := APIError{ Code: code, @@ -80,7 +80,7 @@ func respondWithError(c *gin.Context, status int, code string, message string, d Retryable: isRetryableError(status), } - // respondWithError(c, http.StatusServiceUnavailable, ErrCodeServiceUnavailable, "service unavailable", "database offline") // adds a retry suggestion. + // respondWithError(c, http.StatusServiceUnavailable, ErrCodeServiceUnavailable, "service unavailable", "database offline") adds a retry suggestion. switch code { case ErrCodeMinerNotFound: apiError.Suggestion = "Check the miner name or install the miner first" @@ -125,15 +125,15 @@ func respondWithMiningError(c *gin.Context, err *MiningError) { c.JSON(err.StatusCode(), apiError) } -// isRetryableError(http.StatusServiceUnavailable) // => true -// isRetryableError(http.StatusNotFound) // => false +// isRetryableError(http.StatusServiceUnavailable) returns true. +// isRetryableError(http.StatusNotFound) returns false. func isRetryableError(status int) bool { return status == http.StatusServiceUnavailable || status == http.StatusTooManyRequests || status == http.StatusGatewayTimeout } -// router.Use(securityHeadersMiddleware()) // GET /api/v1/mining/status returns X-Content-Type-Options: nosniff and Content-Security-Policy: default-src 'none' +// router.Use(securityHeadersMiddleware()) adds X-Content-Type-Options: nosniff and Content-Security-Policy: default-src 'none' to GET /api/v1/mining/status. func securityHeadersMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Header("X-Content-Type-Options", "nosniff") @@ -145,7 +145,7 @@ func securityHeadersMiddleware() gin.HandlerFunc { } } -// router.Use(contentTypeValidationMiddleware()) // POST /api/v1/mining/profiles with Content-Type: text/plain returns 415 Unsupported Media Type +// router.Use(contentTypeValidationMiddleware()) returns 415 for POST /api/v1/mining/profiles when Content-Type is text/plain. func contentTypeValidationMiddleware() gin.HandlerFunc { return func(c *gin.Context) { method := c.Request.Method @@ -174,7 +174,7 @@ func contentTypeValidationMiddleware() gin.HandlerFunc { } } -// router.Use(requestIDMiddleware()) // GET /api/v1/mining/status with X-Request-ID: trace-123 keeps the same ID in the response +// router.Use(requestIDMiddleware()) keeps X-Request-ID: trace-123 stable across the request and response. func requestIDMiddleware() gin.HandlerFunc { return func(c *gin.Context) { requestID := c.GetHeader("X-Request-ID") @@ -189,7 +189,7 @@ func requestIDMiddleware() gin.HandlerFunc { } } -// requestID := generateRequestID() // "1712070000123-a1b2c3d4e5f6a7b8" +// requestID := generateRequestID() // example: 1712070000123-a1b2c3d4e5f6a7b8 func generateRequestID() string { randomBytes := make([]byte, 8) if _, err := rand.Read(randomBytes); err != nil { @@ -198,7 +198,7 @@ func generateRequestID() string { return strconv.FormatInt(time.Now().UnixMilli(), 10) + "-" + hex.EncodeToString(randomBytes) } -// requestID := getRequestID(c) // "trace-123" after requestIDMiddleware stores the incoming X-Request-ID header +// 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 { @@ -229,41 +229,36 @@ func logWithRequestID(c *gin.Context, level string, message string, fields loggi } } -// csrfMiddleware protects against CSRF attacks for browser-based requests. -// For state-changing methods (POST, PUT, DELETE), it requires one of: -// - Authorization header (API clients) -// - X-Requested-With header (AJAX clients) -// - Origin header matching allowed origins (already handled by CORS) -// GET requests are always allowed as they should be idempotent. +// csrfMiddleware() allows POST /api/v1/mining/profiles when the request includes Authorization or X-Requested-With. func csrfMiddleware() gin.HandlerFunc { return func(c *gin.Context) { - // Only check state-changing methods + // Only check state-changing methods such as POST /api/v1/mining/profiles. method := c.Request.Method if method == http.MethodGet || method == http.MethodHead || method == http.MethodOptions { c.Next() return } - // Allow if Authorization header present (API client) + // Allow requests like `Authorization: Digest username="miner-admin"` from API clients. if c.GetHeader("Authorization") != "" { c.Next() return } - // Allow if X-Requested-With header present (AJAX/XHR request) + // Allow requests like `X-Requested-With: XMLHttpRequest` from browser clients. if c.GetHeader("X-Requested-With") != "" { c.Next() return } - // Allow if Content-Type is application/json (not sent by HTML forms) + // Allow requests like `Content-Type: application/json` from API clients. contentType := c.GetHeader("Content-Type") if strings.HasPrefix(contentType, "application/json") { c.Next() return } - // Reject the request as potential CSRF + // Reject requests like `POST /api/v1/mining/profiles` from a plain HTML form. respondWithError(c, http.StatusForbidden, "CSRF_PROTECTION", "Request blocked by CSRF protection", "Include X-Requested-With header or use application/json content type") @@ -271,10 +266,10 @@ func csrfMiddleware() gin.HandlerFunc { } } -// ctx, cancel := context.WithTimeout(context.Background(), DefaultRequestTimeout) +// context.WithTimeout(context.Background(), DefaultRequestTimeout) keeps long API requests under 30 seconds. const DefaultRequestTimeout = 30 * time.Second -// Cache-Control header constants +// CachePublic5Min matches GET /api/v1/mining/miners/available responses. const ( CacheNoStore = "no-store" CacheNoCache = "no-cache" @@ -282,10 +277,10 @@ const ( CachePublic5Min = "public, max-age=300" ) -// router.Use(cacheMiddleware()) // serves /miners/available with Cache-Control: public, max-age=300 +// router.Use(cacheMiddleware()) serves GET /miners/available with Cache-Control: public, max-age=300. func cacheMiddleware() gin.HandlerFunc { return func(c *gin.Context) { - // Only cache GET requests + // Only cache GET requests like /api/v1/mining/info. if c.Request.Method != http.MethodGet { c.Header("Cache-Control", CacheNoStore) c.Next() @@ -294,19 +289,18 @@ func cacheMiddleware() gin.HandlerFunc { path := c.Request.URL.Path - // Static-ish resources that can be cached briefly + // Cache GET /api/v1/mining/miners/available for 5 minutes. switch { case strings.HasSuffix(path, "/available"): - // Available miners list - can be cached for 5 minutes c.Header("Cache-Control", CachePublic5Min) case strings.HasSuffix(path, "/info"): - // System info - can be cached for 1 minute + // Cache GET /api/v1/mining/info for 1 minute. c.Header("Cache-Control", CachePublic1Min) case strings.Contains(path, "/swagger"): - // Swagger docs - can be cached + // Cache GET /api/v1/mining/swagger/index.html for 5 minutes. c.Header("Cache-Control", CachePublic5Min) default: - // Dynamic data (stats, miners, profiles) - don't cache + // Keep dynamic requests like /api/v1/mining/miners/xmrig live. c.Header("Cache-Control", CacheNoCache) } @@ -314,10 +308,10 @@ func cacheMiddleware() gin.HandlerFunc { } } -// router.Use(requestTimeoutMiddleware(30 * time.Second)) // aborts a slow /history/miners/xmrig request after 30 seconds +// router.Use(requestTimeoutMiddleware(30 * time.Second)) aborts GET /history/miners/xmrig after 30 seconds. func requestTimeoutMiddleware(timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { - // Skip timeout for WebSocket upgrades and streaming endpoints + // Skip timeout for WebSocket upgrades like /ws/events. if c.GetHeader("Upgrade") == "websocket" { c.Next() return @@ -327,11 +321,11 @@ func requestTimeoutMiddleware(timeout time.Duration) gin.HandlerFunc { return } - // Create context with timeout + // Create a request-scoped timeout for requests like GET /api/v1/mining/history/xmrig. ctx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() - // Replace request context + // Replace the incoming request context with the timed one. c.Request = c.Request.WithContext(ctx) var responded int32 @@ -345,9 +339,9 @@ func requestTimeoutMiddleware(timeout time.Duration) gin.HandlerFunc { select { case <-done: - // Request completed normally + // Request completed normally, for example GET /api/v1/mining/status. case <-ctx.Done(): - // Timeout occurred - only respond if handler hasn't already + // Timeout occurred; only respond if the handler has not already written a response. if atomic.CompareAndSwapInt32(&responded, 0, 1) { c.Abort() respondWithError(c, http.StatusGatewayTimeout, ErrCodeTimeout, @@ -357,36 +351,36 @@ func requestTimeoutMiddleware(timeout time.Duration) gin.HandlerFunc { } } -// conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil) // upgrade HTTP to WebSocket +// conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil) upgrades GET /ws/events to WebSocket. var wsUpgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { - // Allow connections from localhost origins only + // Allow browser requests like `Origin: http://localhost:4200`. origin := r.Header.Get("Origin") if origin == "" { - return true // No origin header (non-browser clients) + return true // No Origin header, for example from curl or another non-browser client. } - // Parse the origin URL properly to prevent bypass attacks + // Parse the origin URL properly to prevent bypass attacks. u, err := url.Parse(origin) if err != nil { return false } host := u.Hostname() - // Only allow exact localhost matches + // Allow exact localhost matches like `http://127.0.0.1:4200`. return host == "localhost" || host == "127.0.0.1" || host == "::1" || host == "wails.localhost" }, } -// service, err := mining.NewService(manager, ":9090", "localhost:9090", "api/v1/mining") -func NewService(manager ManagerInterface, listenAddr string, displayAddr string, swaggerNamespace string) (*Service, error) { +// service, err := mining.NewService(manager, ":9090", "localhost:9090", "/api/v1/mining") +func NewService(manager ManagerInterface, listenAddress string, displayAddress string, swaggerNamespace string) (*Service, error) { apiBasePath := "/" + strings.Trim(swaggerNamespace, "/") swaggerUIPath := apiBasePath + "/swagger" docs.SwaggerInfo.Title = "Mining Module API" docs.SwaggerInfo.Version = "1.0" - docs.SwaggerInfo.Host = displayAddr + docs.SwaggerInfo.Host = displayAddress docs.SwaggerInfo.BasePath = apiBasePath instanceName := "swagger_" + strings.ReplaceAll(strings.Trim(swaggerNamespace, "/"), "/", "_") swag.Register(instanceName, docs.SwaggerInfo) @@ -458,13 +452,13 @@ func NewService(manager ManagerInterface, listenAddr string, displayAddr string, NodeService: nodeService, EventHub: eventHub, Server: &http.Server{ - Addr: listenAddr, + Addr: listenAddress, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, ReadHeaderTimeout: 10 * time.Second, }, - DisplayAddr: displayAddr, + DisplayAddr: displayAddress, SwaggerInstanceName: instanceName, APIBasePath: apiBasePath, SwaggerUIPath: swaggerUIPath,