ax(mining): standardize command and repository names
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

Co-authored-by: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-04 07:08:47 +00:00
parent 757e0a9ce4
commit 9ed6c33c42
18 changed files with 161 additions and 177 deletions

View file

@ -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",

View file

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

View file

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

View file

@ -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",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"`

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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