ax(mining): standardize command and repository names
Co-authored-by: Virgil <virgil@lethean.io>
This commit is contained in:
parent
757e0a9ce4
commit
9ed6c33c42
18 changed files with 161 additions and 177 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}()
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue