diff --git a/cmd/mining/cmd/doctor.go b/cmd/mining/cmd/doctor.go index 97d7006..a8a9aca 100644 --- a/cmd/mining/cmd/doctor.go +++ b/cmd/mining/cmd/doctor.go @@ -32,7 +32,7 @@ func validateInstalledMinerCachePath(cacheFilePath string) error { return nil } -// doctorCmd adds `doctor` so `mining doctor` refreshes the miner cache and prints the installed miner summary. +// rootCmd.AddCommand(doctorCmd) exposes `mining doctor`, which refreshes the miner cache and prints the installed miner summary. var doctorCmd = &cobra.Command{ Use: "doctor", Short: "Check and refresh the status of installed miners", diff --git a/cmd/mining/cmd/node.go b/cmd/mining/cmd/node.go index 0c62574..5e8a30e 100644 --- a/cmd/mining/cmd/node.go +++ b/cmd/mining/cmd/node.go @@ -33,9 +33,9 @@ var nodeInitCmd = &cobra.Command{ Short: "Initialize node identity", Long: `Initialize a new node identity with X25519 keypair. This creates the node's cryptographic identity for secure P2P communication.`, - RunE: func(cmd *cobra.Command, args []string) error { - nodeName, _ := cmd.Flags().GetString("name") - roleName, _ := cmd.Flags().GetString("role") + RunE: func(command *cobra.Command, arguments []string) error { + nodeName, _ := command.Flags().GetString("name") + roleName, _ := command.Flags().GetString("role") if nodeName == "" { return fmt.Errorf("--name is required") @@ -83,7 +83,7 @@ var nodeInfoCmd = &cobra.Command{ Use: "info", Short: "Show node identity and status", Long: `Display the current node's identity, role, and connection status.`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(command *cobra.Command, arguments []string) error { nodeManager, err := node.NewNodeManager() if err != nil { return fmt.Errorf("failed to create node manager: %w", err) @@ -121,8 +121,8 @@ var nodeServeCmd = &cobra.Command{ Short: "Start P2P server for remote connections", Long: `Start the P2P WebSocket server to accept connections from other nodes. This allows other nodes to connect, send commands, and receive stats.`, - RunE: func(cmd *cobra.Command, args []string) error { - listenAddress, _ := cmd.Flags().GetString("listen") + RunE: func(command *cobra.Command, arguments []string) error { + listenAddress, _ := command.Flags().GetString("listen") nodeManager, err := node.NewNodeManager() if err != nil { @@ -186,8 +186,8 @@ var nodeResetCmd = &cobra.Command{ Use: "reset", Short: "Delete node identity and start fresh", Long: `Remove the current node identity, keys, and all peer data. Use with caution!`, - RunE: func(cmd *cobra.Command, args []string) error { - force, _ := cmd.Flags().GetBool("force") + RunE: func(command *cobra.Command, arguments []string) error { + force, _ := command.Flags().GetBool("force") nodeManager, err := node.NewNodeManager() if err != nil { diff --git a/cmd/mining/cmd/peer.go b/cmd/mining/cmd/peer.go index 3cd1c4d..03b6fcc 100644 --- a/cmd/mining/cmd/peer.go +++ b/cmd/mining/cmd/peer.go @@ -8,7 +8,7 @@ import ( "github.com/spf13/cobra" ) -// peer add, peer list, peer remove, peer ping, and peer optimal live under this command group. +// rootCmd.AddCommand(peerCmd) exposes `peer add`, `peer list`, `peer remove`, `peer ping`, and `peer optimal`. var peerCmd = &cobra.Command{ Use: "peer", Short: "Manage peer nodes", @@ -21,9 +21,9 @@ var peerAddCmd = &cobra.Command{ Short: "Add a peer node", Long: `Add a new peer node by address. This will initiate a handshake to exchange public keys and establish a secure connection.`, - RunE: func(cmd *cobra.Command, args []string) error { - address, _ := cmd.Flags().GetString("address") - name, _ := cmd.Flags().GetString("name") + RunE: func(command *cobra.Command, arguments []string) error { + address, _ := command.Flags().GetString("address") + name, _ := command.Flags().GetString("name") if address == "" { return fmt.Errorf("--address is required") @@ -163,8 +163,8 @@ var peerOptimalCmd = &cobra.Command{ Short: "Show the optimal peer based on metrics", Long: `Use the Poindexter KD-tree to find the best peer based on ping latency, hop count, geographic distance, and reliability score.`, - RunE: func(cmd *cobra.Command, args []string) error { - count, _ := cmd.Flags().GetInt("count") + RunE: func(command *cobra.Command, arguments []string) error { + count, _ := command.Flags().GetInt("count") peerRegistry, err := getPeerRegistry() if err != nil { diff --git a/cmd/mining/cmd/update.go b/cmd/mining/cmd/update.go index 45cf9a6..9faf096 100644 --- a/cmd/mining/cmd/update.go +++ b/cmd/mining/cmd/update.go @@ -24,7 +24,7 @@ func validateUpdateCacheFilePath(cacheFilePath string) error { return nil } -// updateCmd adds `update` so `mining update` can compare the cached miner version against the latest release. +// rootCmd.AddCommand(updateCmd) exposes `mining update`, which compares the cached miner version against the latest release. var updateCmd = &cobra.Command{ Use: "update", Short: "Check for updates to installed miners", diff --git a/pkg/database/database.go b/pkg/database/database.go index cc0c48c..d6598de 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -87,14 +87,11 @@ func Initialize(config Config) error { return databaseError("open database", err) } - // Set connection pool settings globalDatabase.SetMaxOpenConns(1) // SQLite only supports one writer globalDatabase.SetMaxIdleConns(1) globalDatabase.SetConnMaxLifetime(time.Hour) - // Create tables if err := createTables(); err != nil { - // Nil out global before closing to prevent use of closed connection closingDB := globalDatabase globalDatabase = nil closingDB.Close() diff --git a/pkg/mining/auth_test.go b/pkg/mining/auth_test.go index f0a02c2..fabe078 100644 --- a/pkg/mining/auth_test.go +++ b/pkg/mining/auth_test.go @@ -41,7 +41,6 @@ func TestAuth_DefaultAuthConfig_Good(t *testing.T) { // TestAuth_AuthConfigFromEnv_Good — authConfig := AuthConfigFromEnv() with valid credentials func TestAuth_AuthConfigFromEnv_Good(t *testing.T) { - // Save original env origAuth := os.Getenv("MINING_API_AUTH") origUser := os.Getenv("MINING_API_USER") origPass := os.Getenv("MINING_API_PASS") @@ -129,7 +128,6 @@ func TestAuth_NewDigestAuth_Good(t *testing.T) { t.Fatal("expected non-nil DigestAuth") } - // Cleanup digestAuth.Stop() } @@ -138,7 +136,6 @@ func TestAuth_DigestAuthStop_Ugly(t *testing.T) { authConfig := DefaultAuthConfig() digestAuth := NewDigestAuth(authConfig) - // Should not panic when called multiple times digestAuth.Stop() digestAuth.Stop() digestAuth.Stop() @@ -156,9 +153,9 @@ func TestAuth_Middleware_Good(t *testing.T) { c.String(http.StatusOK, "success") }) - req := httptest.NewRequest("GET", "/test", nil) + request := httptest.NewRequest("GET", "/test", nil) recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Errorf("expected status 200, got %d", recorder.Code) @@ -186,9 +183,9 @@ func TestAuth_Middleware_Bad(t *testing.T) { c.String(http.StatusOK, "success") }) - req := httptest.NewRequest("GET", "/test", nil) + request := httptest.NewRequest("GET", "/test", nil) recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusUnauthorized { t.Errorf("expected status 401, got %d", recorder.Code) @@ -224,10 +221,10 @@ func TestAuth_BasicAuth_Good(t *testing.T) { c.String(http.StatusOK, "success") }) - req := httptest.NewRequest("GET", "/test", nil) - req.SetBasicAuth("user", "pass") + request := httptest.NewRequest("GET", "/test", nil) + request.SetBasicAuth("user", "pass") recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Errorf("expected status 200, got %d", recorder.Code) @@ -266,10 +263,10 @@ func TestAuth_BasicAuth_Bad(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - req := httptest.NewRequest("GET", "/test", nil) - req.SetBasicAuth(testCase.user, testCase.password) + request := httptest.NewRequest("GET", "/test", nil) + request.SetBasicAuth(testCase.user, testCase.password) recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusUnauthorized { t.Errorf("expected status 401, got %d", recorder.Code) @@ -297,9 +294,9 @@ func TestAuth_DigestAuth_Good(t *testing.T) { }) // First request to get nonce - req := httptest.NewRequest("GET", "/test", nil) + request := httptest.NewRequest("GET", "/test", nil) recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusUnauthorized { t.Fatalf("expected 401 to get nonce, got %d", recorder.Code) @@ -329,10 +326,10 @@ func TestAuth_DigestAuth_Good(t *testing.T) { ) // Second request with digest auth - req2 := httptest.NewRequest("GET", "/test", nil) - req2.Header.Set("Authorization", authHeader) + secondRequest := httptest.NewRequest("GET", "/test", nil) + secondRequest.Header.Set("Authorization", authHeader) authRecorder := httptest.NewRecorder() - router.ServeHTTP(authRecorder, req2) + router.ServeHTTP(authRecorder, secondRequest) if authRecorder.Code != http.StatusOK { t.Errorf("expected status 200, got %d; body: %s", authRecorder.Code, authRecorder.Body.String()) @@ -359,10 +356,10 @@ func TestAuth_DigestAuth_Bad(t *testing.T) { // Try with a fake nonce that was never issued authHeader := `Digest username="user", realm="Test", nonce="fakenonce123", uri="/test", qop=auth, nc=00000001, cnonce="abc", response="xxx"` - req := httptest.NewRequest("GET", "/test", nil) - req.Header.Set("Authorization", authHeader) + request := httptest.NewRequest("GET", "/test", nil) + request.Header.Set("Authorization", authHeader) recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusUnauthorized { t.Errorf("expected status 401 for invalid nonce, got %d", recorder.Code) @@ -388,9 +385,9 @@ func TestAuth_DigestAuth_Ugly(t *testing.T) { }) // Get a valid nonce - req := httptest.NewRequest("GET", "/test", nil) + request := httptest.NewRequest("GET", "/test", nil) recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) wwwAuth := recorder.Header().Get("WWW-Authenticate") params := parseDigestParams(wwwAuth[7:]) @@ -410,10 +407,10 @@ func TestAuth_DigestAuth_Ugly(t *testing.T) { authConfig.Username, authConfig.Realm, nonce, uri, response, ) - req2 := httptest.NewRequest("GET", "/test", nil) - req2.Header.Set("Authorization", authHeader) + secondRequest := httptest.NewRequest("GET", "/test", nil) + secondRequest.Header.Set("Authorization", authHeader) w2 := httptest.NewRecorder() - router.ServeHTTP(w2, req2) + router.ServeHTTP(w2, secondRequest) if w2.Code != http.StatusUnauthorized { t.Errorf("expected status 401 for expired nonce, got %d", w2.Code) @@ -589,7 +586,6 @@ func TestAuth_NonceCleanup_Ugly(t *testing.T) { } } -// Helper function func authTestContains(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { @@ -599,7 +595,6 @@ func authTestContains(s, substr string) bool { return false } -// Benchmark tests func BenchmarkMd5Hash(b *testing.B) { input := "user:realm:password" for i := 0; i < b.N; i++ { @@ -634,12 +629,12 @@ func BenchmarkBasicAuthValidation(b *testing.B) { c.Status(http.StatusOK) }) - req := httptest.NewRequest("GET", "/test", nil) - req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("user:pass"))) + request := httptest.NewRequest("GET", "/test", nil) + request.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("user:pass"))) b.ResetTimer() for i := 0; i < b.N; i++ { recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) } } diff --git a/pkg/mining/manager.go b/pkg/mining/manager.go index aa4db99..02e73a7 100644 --- a/pkg/mining/manager.go +++ b/pkg/mining/manager.go @@ -71,7 +71,7 @@ type Manager struct { databaseEnabled bool databaseRetention int eventHub *EventHub - eventHubMutex sync.RWMutex // Separate mutex for eventHub to avoid deadlock with main mutex + eventHubMutex sync.RWMutex } // manager.SetEventHub(eventHub) @@ -122,7 +122,7 @@ func NewManagerForSimulation() *Manager { return manager } -// manager.initDatabase() loads `miners.json` and enables database persistence when `Database.Enabled` is true. +// manager.initDatabase() reads `~/.config/lethean-desktop/miners.json` and enables database persistence when `Database.Enabled` is true. func (manager *Manager) initDatabase() { minersConfig, err := LoadMinersConfig() if err != nil { @@ -154,11 +154,11 @@ func (manager *Manager) initDatabase() { logging.Info("database persistence enabled", logging.Fields{"retention_days": manager.databaseRetention}) - // manager.startDBCleanup() runs once per hour after NewManager enables persistence. + // manager.startDBCleanup() calls database.Cleanup(30) at startup and every hour after NewManager enables persistence. manager.startDBCleanup() } -// manager.startDBCleanup() runs `database.Cleanup(manager.databaseRetention)` on startup and every hour. +// manager.startDBCleanup() calls database.Cleanup(manager.databaseRetention) at startup and every hour. func (manager *Manager) startDBCleanup() { manager.waitGroup.Add(1) go func() { @@ -168,11 +168,9 @@ func (manager *Manager) startDBCleanup() { logging.Error("panic in database cleanup goroutine", logging.Fields{"panic": r}) } }() - // cleanupTicker := time.NewTicker(time.Hour) checks for expired rows every 60 minutes. cleanupTicker := time.NewTicker(time.Hour) defer cleanupTicker.Stop() - // database.Cleanup(manager.databaseRetention) removes rows older than the configured retention period during startup. if err := database.Cleanup(manager.databaseRetention); err != nil { logging.Warn("database cleanup failed", logging.Fields{"error": err}) } @@ -190,7 +188,7 @@ func (manager *Manager) startDBCleanup() { }() } -// manager.syncMinersConfig() adds missing entries such as `MinerAutostartConfig{MinerType: "xmrig", Autostart: false}`. +// manager.syncMinersConfig() appends `MinerAutostartConfig{MinerType: "xmrig", Autostart: false}` to miners.json when a miner is missing. func (manager *Manager) syncMinersConfig() { minersConfig, err := LoadMinersConfig() if err != nil { diff --git a/pkg/mining/miner.go b/pkg/mining/miner.go index 7b6d5bc..62ea050 100644 --- a/pkg/mining/miner.go +++ b/pkg/mining/miner.go @@ -23,7 +23,7 @@ import ( ) // buffer := NewLogBuffer(500) -// cmd.Stdout = buffer // satisfies io.Writer; ring-buffers miner output +// command.Stdout = buffer // satisfies io.Writer; ring-buffers miner output type LogBuffer struct { lines []string maxLines int @@ -31,7 +31,7 @@ type LogBuffer struct { } // buffer := NewLogBuffer(500) -// cmd.Stdout = buffer +// command.Stdout = buffer func NewLogBuffer(maxLines int) *LogBuffer { return &LogBuffer{ lines: make([]string, 0, maxLines), @@ -42,7 +42,7 @@ func NewLogBuffer(maxLines int) *LogBuffer { // if len(line) > maxLineLength { line = line[:maxLineLength] + "... [truncated]" } const maxLineLength = 2000 -// cmd.Stdout = lb // satisfies io.Writer; timestamps and ring-buffers each line +// command.Stdout = lb // satisfies io.Writer; timestamps and ring-buffers each line func (logBuffer *LogBuffer) Write(p []byte) (n int, err error) { logBuffer.mutex.Lock() defer logBuffer.mutex.Unlock() @@ -104,7 +104,7 @@ type BaseMiner struct { ConfigPath string `json:"configPath"` API *API `json:"api"` mutex sync.RWMutex - cmd *exec.Cmd + command *exec.Cmd stdinPipe io.WriteCloser `json:"-"` HashrateHistory []HashratePoint `json:"hashrateHistory"` LowResHashrateHistory []HashratePoint `json:"lowResHashrateHistory"` @@ -148,7 +148,7 @@ func (b *BaseMiner) GetBinaryPath() string { func (b *BaseMiner) Stop() error { b.mutex.Lock() - if !b.Running || b.cmd == nil { + if !b.Running || b.command == nil { b.mutex.Unlock() return ErrMinerNotRunning(b.Name) } @@ -159,13 +159,13 @@ func (b *BaseMiner) Stop() error { b.stdinPipe = nil } - // Capture cmd locally to avoid race with Wait() goroutine - cmd := b.cmd - process := cmd.Process + // Capture command locally to avoid race with the Wait() goroutine. + command := b.command + process := command.Process // Mark as not running immediately to prevent concurrent Stop() calls b.Running = false - b.cmd = nil + b.command = nil b.mutex.Unlock() // Try graceful shutdown with SIGTERM first (Unix only) @@ -396,10 +396,10 @@ func (b *BaseMiner) CheckInstallation() (*InstallationDetails, error) { b.MinerBinary = binaryPath b.Path = filepath.Dir(binaryPath) - cmd := exec.Command(binaryPath, "--version") + command := exec.Command(binaryPath, "--version") var output bytes.Buffer - cmd.Stdout = &output - if err := cmd.Run(); err != nil { + command.Stdout = &output + if err := command.Run(); err != nil { b.Version = "Unknown (could not run executable)" } else { fields := strings.Fields(output.String()) diff --git a/pkg/mining/ratelimiter_test.go b/pkg/mining/ratelimiter_test.go index 8cdc014..be75655 100644 --- a/pkg/mining/ratelimiter_test.go +++ b/pkg/mining/ratelimiter_test.go @@ -53,10 +53,10 @@ func TestRatelimiter_Middleware_Good(t *testing.T) { }) for i := 0; i < 5; i++ { - req := httptest.NewRequest("GET", "/test", nil) - req.RemoteAddr = "192.168.1.1:12345" + request := httptest.NewRequest("GET", "/test", nil) + request.RemoteAddr = "192.168.1.1:12345" recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Errorf("Request %d: expected 200, got %d", i+1, recorder.Code) @@ -77,16 +77,16 @@ func TestRatelimiter_Middleware_Bad(t *testing.T) { }) for i := 0; i < 5; i++ { - req := httptest.NewRequest("GET", "/test", nil) - req.RemoteAddr = "192.168.1.1:12345" + request := httptest.NewRequest("GET", "/test", nil) + request.RemoteAddr = "192.168.1.1:12345" recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) } - req := httptest.NewRequest("GET", "/test", nil) - req.RemoteAddr = "192.168.1.1:12345" + request := httptest.NewRequest("GET", "/test", nil) + request.RemoteAddr = "192.168.1.1:12345" recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusTooManyRequests { t.Errorf("Expected 429 Too Many Requests, got %d", recorder.Code) @@ -106,24 +106,24 @@ func TestRatelimiter_Middleware_Ugly(t *testing.T) { }) for i := 0; i < 2; i++ { - req := httptest.NewRequest("GET", "/test", nil) - req.RemoteAddr = "192.168.1.1:12345" + request := httptest.NewRequest("GET", "/test", nil) + request.RemoteAddr = "192.168.1.1:12345" recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) } - req := httptest.NewRequest("GET", "/test", nil) - req.RemoteAddr = "192.168.1.1:12345" + request := httptest.NewRequest("GET", "/test", nil) + request.RemoteAddr = "192.168.1.1:12345" recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusTooManyRequests { t.Errorf("IP1 should be rate limited, got %d", recorder.Code) } - req = httptest.NewRequest("GET", "/test", nil) - req.RemoteAddr = "192.168.1.2:12345" + request = httptest.NewRequest("GET", "/test", nil) + request.RemoteAddr = "192.168.1.2:12345" recorder = httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Errorf("IP2 should not be rate limited, got %d", recorder.Code) } @@ -145,19 +145,19 @@ func TestRatelimiter_ClientCount_Good(t *testing.T) { t.Errorf("Expected 0 clients, got %d", count) } - req := httptest.NewRequest("GET", "/test", nil) - req.RemoteAddr = "192.168.1.1:12345" + request := httptest.NewRequest("GET", "/test", nil) + request.RemoteAddr = "192.168.1.1:12345" recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if count := rateLimiter.ClientCount(); count != 1 { t.Errorf("Expected 1 client, got %d", count) } - req = httptest.NewRequest("GET", "/test", nil) - req.RemoteAddr = "192.168.1.2:12345" + request = httptest.NewRequest("GET", "/test", nil) + request.RemoteAddr = "192.168.1.2:12345" recorder = httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if count := rateLimiter.ClientCount(); count != 2 { t.Errorf("Expected 2 clients, got %d", count) @@ -176,28 +176,28 @@ func TestRatelimiter_TokenRefill_Good(t *testing.T) { c.String(http.StatusOK, "ok") }) - req := httptest.NewRequest("GET", "/test", nil) - req.RemoteAddr = "192.168.1.1:12345" + request := httptest.NewRequest("GET", "/test", nil) + request.RemoteAddr = "192.168.1.1:12345" recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Errorf("First request should succeed, got %d", recorder.Code) } - req = httptest.NewRequest("GET", "/test", nil) - req.RemoteAddr = "192.168.1.1:12345" + request = httptest.NewRequest("GET", "/test", nil) + request.RemoteAddr = "192.168.1.1:12345" recorder = httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusTooManyRequests { t.Errorf("Second request should be rate limited, got %d", recorder.Code) } time.Sleep(20 * time.Millisecond) - req = httptest.NewRequest("GET", "/test", nil) - req.RemoteAddr = "192.168.1.1:12345" + request = httptest.NewRequest("GET", "/test", nil) + request.RemoteAddr = "192.168.1.1:12345" recorder = httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Errorf("Third request should succeed after refill, got %d", recorder.Code) } diff --git a/pkg/mining/repository_test.go b/pkg/mining/repository_test.go index 4a82c74..9e43983 100644 --- a/pkg/mining/repository_test.go +++ b/pkg/mining/repository_test.go @@ -285,6 +285,12 @@ func TestFileRepository_SaveToReadOnlyDirectory_Bad(t *testing.T) { } defer os.Chmod(readOnlyDir, 0755) // Restore permissions for cleanup + probeFilePath := filepath.Join(readOnlyDir, ".permission-probe") + if err := os.WriteFile(probeFilePath, []byte("probe"), 0600); err == nil { + os.Remove(probeFilePath) + t.Skip("filesystem does not enforce read-only directory permissions") + } + path := filepath.Join(readOnlyDir, "test.json") repo := NewFileRepository[testData](path) diff --git a/pkg/mining/service.go b/pkg/mining/service.go index 48bd8b0..ec1b195 100644 --- a/pkg/mining/service.go +++ b/pkg/mining/service.go @@ -60,7 +60,6 @@ type APIError struct { Retryable bool `json:"retryable"` // Can the client retry? } -// 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") returns "" in production and the full string in debug mode. @@ -80,7 +79,6 @@ func respondWithError(requestContext *gin.Context, status int, code string, mess Retryable: isRetryableError(status), } - // respondWithError(requestContext, 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" @@ -133,7 +131,7 @@ func isRetryableError(status int) bool { status == http.StatusGatewayTimeout } -// router.Use(securityHeadersMiddleware()) adds X-Content-Type-Options: nosniff and Content-Security-Policy: default-src 'none' to GET /api/v1/mining/status. +// 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(requestContext *gin.Context) { requestContext.Header("X-Content-Type-Options", "nosniff") @@ -232,26 +230,26 @@ func logWithRequestContext(requestContext *gin.Context, level string, message st // csrfMiddleware() allows POST /api/v1/mining/profiles when the request includes Authorization or X-Requested-With. func csrfMiddleware() gin.HandlerFunc { return func(requestContext *gin.Context) { - // Only check state-changing methods such as POST /api/v1/mining/profiles. + // requestContext.Request.Method == http.MethodPost keeps POST /api/v1/mining/profiles protected. method := requestContext.Request.Method if method == http.MethodGet || method == http.MethodHead || method == http.MethodOptions { requestContext.Next() return } - // Allow requests like `Authorization: Digest username="miner-admin"` from API clients. + // requestContext.GetHeader("Authorization") != "" allows requests like `Authorization: Digest username="miner-admin"`. if requestContext.GetHeader("Authorization") != "" { requestContext.Next() return } - // Allow requests like `X-Requested-With: XMLHttpRequest` from browser clients. + // requestContext.GetHeader("X-Requested-With") != "" allows requests like `X-Requested-With: XMLHttpRequest`. if requestContext.GetHeader("X-Requested-With") != "" { requestContext.Next() return } - // Allow requests like `Content-Type: application/json` from API clients. + // strings.HasPrefix(contentType, "application/json") allows requests like `Content-Type: application/json`. contentType := requestContext.GetHeader("Content-Type") if strings.HasPrefix(contentType, "application/json") { requestContext.Next() @@ -678,7 +676,7 @@ func (service *Service) SetupRoutes() { logging.Info("MCP server enabled", logging.Fields{"endpoint": service.APIBasePath + "/mcp"}) } -// requestContext.JSON(http.StatusOK, HealthResponse{Status: "healthy", Components: map[string]string{"db": "ok"}}) +// requestContext.JSON(http.StatusOK, HealthResponse{Status: "healthy", Components: map[string]string{"database": "ok"}}) type HealthResponse struct { Status string `json:"status"` Components map[string]string `json:"components,omitempty"` diff --git a/pkg/mining/service_test.go b/pkg/mining/service_test.go index def5776..8540145 100644 --- a/pkg/mining/service_test.go +++ b/pkg/mining/service_test.go @@ -125,9 +125,9 @@ func TestService_HandleListMiners_Good(t *testing.T) { return []Miner{&XMRigMiner{BaseMiner: BaseMiner{Name: "test-miner"}}} } - req, _ := http.NewRequest("GET", "/miners", nil) + request, _ := http.NewRequest("GET", "/miners", nil) recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Errorf("expected status %d, got %d", http.StatusOK, recorder.Code) @@ -137,10 +137,9 @@ func TestService_HandleListMiners_Good(t *testing.T) { func TestService_HandleGetInfo_Good(t *testing.T) { router, _ := setupTestRouter() - // Case 1: Successful response - req, _ := http.NewRequest("GET", "/info", nil) + request, _ := http.NewRequest("GET", "/info", nil) recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Errorf("expected status %d, got %d", http.StatusOK, recorder.Code) @@ -153,10 +152,9 @@ func TestService_HandleDoctor_Good(t *testing.T) { return []AvailableMiner{{Name: "xmrig"}} } - // Case 1: Successful response - req, _ := http.NewRequest("POST", "/doctor", nil) + request, _ := http.NewRequest("POST", "/doctor", nil) recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Errorf("expected status %d, got %d", http.StatusOK, recorder.Code) @@ -166,13 +164,11 @@ func TestService_HandleDoctor_Good(t *testing.T) { func TestService_HandleInstallMiner_Good(t *testing.T) { router, _ := setupTestRouter() - // Test installing a miner - req, _ := http.NewRequest("POST", "/miners/xmrig/install", nil) - req.Header.Set("Content-Type", "application/json") + request, _ := http.NewRequest("POST", "/miners/xmrig/install", nil) + request.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) - // Installation endpoint should be accessible if recorder.Code != http.StatusOK && recorder.Code != http.StatusInternalServerError { t.Errorf("expected status 200 or 500, got %d", recorder.Code) } @@ -184,9 +180,9 @@ func TestService_HandleStopMiner_Good(t *testing.T) { return nil } - req, _ := http.NewRequest("DELETE", "/miners/test-miner", nil) + request, _ := http.NewRequest("DELETE", "/miners/test-miner", nil) recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Errorf("expected status %d, got %d", http.StatusOK, recorder.Code) @@ -204,9 +200,9 @@ func TestService_HandleGetMinerStats_Good(t *testing.T) { }, nil } - req, _ := http.NewRequest("GET", "/miners/test-miner/stats", nil) + request, _ := http.NewRequest("GET", "/miners/test-miner/stats", nil) recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Errorf("expected status %d, got %d", http.StatusOK, recorder.Code) @@ -219,9 +215,9 @@ func TestService_HandleGetMinerHashrateHistory_Good(t *testing.T) { return []HashratePoint{{Timestamp: time.Now(), Hashrate: 100}}, nil } - req, _ := http.NewRequest("GET", "/miners/test-miner/hashrate-history", nil) + request, _ := http.NewRequest("GET", "/miners/test-miner/hashrate-history", nil) recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) + router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Errorf("expected status %d, got %d", http.StatusOK, recorder.Code) diff --git a/pkg/mining/ttminer.go b/pkg/mining/ttminer.go index b48e5ef..66ee87d 100644 --- a/pkg/mining/ttminer.go +++ b/pkg/mining/ttminer.go @@ -147,11 +147,11 @@ func (m *TTMiner) CheckInstallation() (*InstallationDetails, error) { } // Run version command before acquiring lock (I/O operation) - cmd := exec.Command(binaryPath, "--version") + command := exec.Command(binaryPath, "--version") var commandOutput bytes.Buffer - cmd.Stdout = &commandOutput + command.Stdout = &commandOutput var version string - if err := cmd.Run(); err != nil { + if err := command.Run(); err != nil { version = "Unknown (could not run executable)" } else { // Parse version from output diff --git a/pkg/mining/ttminer_start.go b/pkg/mining/ttminer_start.go index abb9f00..7acfe09 100644 --- a/pkg/mining/ttminer_start.go +++ b/pkg/mining/ttminer_start.go @@ -42,10 +42,10 @@ func (m *TTMiner) Start(config *Config) error { logging.Info("executing TT-Miner command", logging.Fields{"binary": m.MinerBinary, "args": strings.Join(args, " ")}) - m.cmd = exec.Command(m.MinerBinary, args...) + m.command = exec.Command(m.MinerBinary, args...) // Create stdin pipe for console commands - stdinPipe, err := m.cmd.StdinPipe() + stdinPipe, err := m.command.StdinPipe() if err != nil { return ErrStartFailed(m.Name).WithDetails("failed to create stdin pipe").WithCause(err) } @@ -53,29 +53,29 @@ func (m *TTMiner) Start(config *Config) error { // Always capture output to LogBuffer if m.LogBuffer != nil { - m.cmd.Stdout = m.LogBuffer - m.cmd.Stderr = m.LogBuffer + m.command.Stdout = m.LogBuffer + m.command.Stderr = m.LogBuffer } // Also output to console if requested if config.LogOutput { - m.cmd.Stdout = io.MultiWriter(m.LogBuffer, os.Stdout) - m.cmd.Stderr = io.MultiWriter(m.LogBuffer, os.Stderr) + m.command.Stdout = io.MultiWriter(m.LogBuffer, os.Stdout) + m.command.Stderr = io.MultiWriter(m.LogBuffer, os.Stderr) } - if err := m.cmd.Start(); err != nil { + if err := m.command.Start(); err != nil { stdinPipe.Close() return ErrStartFailed(m.Name).WithDetails("process start failed").WithCause(err) } m.Running = true - // Capture cmd locally to avoid race with Stop() - cmd := m.cmd + // Capture command locally to avoid race with the Stop() method. + command := m.command go func() { // Use a channel to detect if Wait() completes done := make(chan error, 1) go func() { - done <- cmd.Wait() + done <- command.Wait() }() // Wait with timeout to prevent goroutine leak on zombie processes @@ -86,8 +86,8 @@ func (m *TTMiner) Start(config *Config) error { case <-time.After(5 * time.Minute): // Process didn't exit after 5 minutes - force cleanup logging.Warn("TT-Miner process wait timeout, forcing cleanup") - if cmd.Process != nil { - cmd.Process.Kill() + if command.Process != nil { + command.Process.Kill() } // Wait for inner goroutine with secondary timeout to prevent leak select { @@ -101,9 +101,9 @@ func (m *TTMiner) Start(config *Config) error { m.mutex.Lock() // Only clear if this is still the same command (not restarted) - if m.cmd == cmd { + if m.command == command { m.Running = false - m.cmd = nil + m.command = nil } m.mutex.Unlock() if err != nil { diff --git a/pkg/mining/xmrig.go b/pkg/mining/xmrig.go index 11ad2fc..979eaa1 100644 --- a/pkg/mining/xmrig.go +++ b/pkg/mining/xmrig.go @@ -158,11 +158,11 @@ func (m *XMRigMiner) CheckInstallation() (*InstallationDetails, error) { } // Run version command before acquiring lock (I/O operation) - cmd := exec.Command(binaryPath, "--version") + command := exec.Command(binaryPath, "--version") var output bytes.Buffer - cmd.Stdout = &output + command.Stdout = &output var version string - if err := cmd.Run(); err != nil { + if err := command.Run(); err != nil { version = "Unknown (could not run executable)" } else { fields := strings.Fields(output.String()) diff --git a/pkg/mining/xmrig_start.go b/pkg/mining/xmrig_start.go index aec7ae2..e5557ad 100644 --- a/pkg/mining/xmrig_start.go +++ b/pkg/mining/xmrig_start.go @@ -66,10 +66,10 @@ func (m *XMRigMiner) Start(config *Config) error { logging.Info("executing miner command", logging.Fields{"binary": m.MinerBinary, "args": strings.Join(args, " ")}) - m.cmd = exec.Command(m.MinerBinary, args...) + m.command = exec.Command(m.MinerBinary, args...) // Create stdin pipe for console commands - stdinPipe, err := m.cmd.StdinPipe() + stdinPipe, err := m.command.StdinPipe() if err != nil { return ErrStartFailed(m.Name).WithDetails("failed to create stdin pipe").WithCause(err) } @@ -77,16 +77,16 @@ func (m *XMRigMiner) Start(config *Config) error { // Always capture output to LogBuffer if m.LogBuffer != nil { - m.cmd.Stdout = m.LogBuffer - m.cmd.Stderr = m.LogBuffer + m.command.Stdout = m.LogBuffer + m.command.Stderr = m.LogBuffer } // Also output to console if requested if config.LogOutput { - m.cmd.Stdout = io.MultiWriter(m.LogBuffer, os.Stdout) - m.cmd.Stderr = io.MultiWriter(m.LogBuffer, os.Stderr) + m.command.Stdout = io.MultiWriter(m.LogBuffer, os.Stdout) + m.command.Stderr = io.MultiWriter(m.LogBuffer, os.Stderr) } - if err := m.cmd.Start(); err != nil { + if err := m.command.Start(); err != nil { stdinPipe.Close() // Clean up config file on failed start if m.ConfigPath != "" { @@ -97,15 +97,15 @@ func (m *XMRigMiner) Start(config *Config) error { m.Running = true - // Capture cmd locally to avoid race with Stop() - cmd := m.cmd + // Capture command locally to avoid race with the Stop() method. + command := m.command minerName := m.Name // Capture name for logging go func() { // Use a channel to detect if Wait() completes done := make(chan struct{}) var waitErr error go func() { - waitErr = cmd.Wait() + waitErr = command.Wait() close(done) }() @@ -126,8 +126,8 @@ func (m *XMRigMiner) Start(config *Config) error { case <-time.After(5 * time.Minute): // Process didn't exit after 5 minutes - force cleanup logging.Warn("miner process wait timeout, forcing cleanup", logging.Fields{"miner": minerName}) - if cmd.Process != nil { - cmd.Process.Kill() + if command.Process != nil { + command.Process.Kill() } // Wait with timeout to prevent goroutine leak if Wait() never returns select { @@ -140,9 +140,9 @@ func (m *XMRigMiner) Start(config *Config) error { m.mutex.Lock() // Only clear if this is still the same command (not restarted) - if m.cmd == cmd { + if m.command == command { m.Running = false - m.cmd = nil + m.command = nil } m.mutex.Unlock() }() diff --git a/pkg/mining/xmrig_test.go b/pkg/mining/xmrig_test.go index 4fd5f4a..b91a9c6 100644 --- a/pkg/mining/xmrig_test.go +++ b/pkg/mining/xmrig_test.go @@ -16,11 +16,11 @@ import ( ) // MockRoundTripper is a mock implementation of http.RoundTripper for testing. -type MockRoundTripper func(req *http.Request) *http.Response +type MockRoundTripper func(request *http.Request) *http.Response // response, _ := MockRoundTripper(fn).RoundTrip(request) -func (f MockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - return f(req), nil +func (f MockRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { + return f(request), nil } // newTestClient returns *http.Client with Transport replaced to avoid making real calls. @@ -30,7 +30,7 @@ func newTestClient(fn MockRoundTripper) *http.Client { } } -// helper function to create a temporary directory for testing +// tempDir(t) returns a directory such as "/tmp/xmrig-test-123456789". func tempDir(t *testing.T) string { dir, err := os.MkdirTemp("", "test") if err != nil { @@ -65,8 +65,8 @@ func TestXMRigMiner_GetName_Good(t *testing.T) { func TestXMRigMiner_GetLatestVersion_Good(t *testing.T) { originalClient := getHTTPClient() - setHTTPClient(newTestClient(func(req *http.Request) *http.Response { - if req.URL.String() != "https://api.github.com/repos/xmrig/xmrig/releases/latest" { + setHTTPClient(newTestClient(func(request *http.Request) *http.Response { + if request.URL.String() != "https://api.github.com/repos/xmrig/xmrig/releases/latest" { return &http.Response{ StatusCode: http.StatusNotFound, Body: io.NopCloser(strings.NewReader("Not Found")), @@ -93,7 +93,7 @@ func TestXMRigMiner_GetLatestVersion_Good(t *testing.T) { func TestXMRigMiner_GetLatestVersion_Bad(t *testing.T) { originalClient := getHTTPClient() - setHTTPClient(newTestClient(func(req *http.Request) *http.Response { + setHTTPClient(newTestClient(func(request *http.Request) *http.Response { return &http.Response{ StatusCode: http.StatusNotFound, Body: io.NopCloser(strings.NewReader("Not Found")), @@ -119,7 +119,6 @@ func TestXMRigMiner_Start_Stop_Bad(t *testing.T) { func TestXMRigMiner_CheckInstallation_Good(t *testing.T) { tmpDir := t.TempDir() - // Use "miner" since that's what NewXMRigMiner() sets as ExecutableName executableName := "miner" if runtime.GOOS == "windows" { executableName += ".exe" @@ -127,24 +126,20 @@ func TestXMRigMiner_CheckInstallation_Good(t *testing.T) { dummyExePath := filepath.Join(tmpDir, executableName) if runtime.GOOS == "windows" { - // Create a dummy batch file that prints version if err := os.WriteFile(dummyExePath, []byte("@echo off\necho XMRig 6.24.0\n"), 0755); err != nil { t.Fatalf("failed to create dummy executable: %v", err) } } else { - // Create a dummy shell script that prints version if err := os.WriteFile(dummyExePath, []byte("#!/bin/sh\necho 'XMRig 6.24.0'\n"), 0755); err != nil { t.Fatalf("failed to create dummy executable: %v", err) } } - // Prepend tmpDir to PATH so findMinerBinary can find it originalPath := os.Getenv("PATH") t.Cleanup(func() { os.Setenv("PATH", originalPath) }) os.Setenv("PATH", tmpDir+string(os.PathListSeparator)+originalPath) miner := NewXMRigMiner() - // Clear any binary path to force search miner.MinerBinary = "" details, err := miner.CheckInstallation() @@ -157,7 +152,6 @@ func TestXMRigMiner_CheckInstallation_Good(t *testing.T) { if details.Version != "6.24.0" { t.Errorf("Expected version '6.24.0', got '%s'", details.Version) } - // On Windows, the path might be canonicalized differently (e.g. 8.3 names), so checking Base is safer or full path equality if we trust os.Path if filepath.Base(details.MinerBinary) != executableName { t.Errorf("Expected binary name '%s', got '%s'", executableName, filepath.Base(details.MinerBinary)) } diff --git a/pkg/node/protocol.go b/pkg/node/protocol.go index ac6935f..8ea4be6 100644 --- a/pkg/node/protocol.go +++ b/pkg/node/protocol.go @@ -15,7 +15,7 @@ func (protocolError *ProtocolError) Error() string { } // handler := &ResponseHandler{} -// if err := handler.ValidateResponse(resp, MsgPong); err != nil { return 0, err } +// if err := handler.ValidateResponse(response, MsgPong); err != nil { return 0, err } type ResponseHandler struct{} // if err := handler.ValidateResponse(response, MsgPong); err != nil { return 0, err }