diff --git a/cmd/mining/cmd/serve.go b/cmd/mining/cmd/serve.go index fdecadf..d1f3a86 100644 --- a/cmd/mining/cmd/serve.go +++ b/cmd/mining/cmd/serve.go @@ -12,6 +12,8 @@ import ( "github.com/Snider/Mining/pkg/mining" "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) var ( @@ -121,7 +123,7 @@ var serveCmd = &cobra.Command{ if err != nil { fmt.Fprintf(os.Stderr, "Error getting miner stats: %v\n", err) } else { - fmt.Printf("Miner Status for %s:\n", strings.Title(minerName)) + fmt.Printf("Miner Status for %s:\n", cases.Title(language.English).String(minerName)) fmt.Printf(" Hash Rate: %d H/s\n", stats.Hashrate) fmt.Printf(" Shares: %d\n", stats.Shares) fmt.Printf(" Rejected: %d\n", stats.Rejected) diff --git a/cmd/mining/cmd/status.go b/cmd/mining/cmd/status.go index d026cea..abea9d8 100644 --- a/cmd/mining/cmd/status.go +++ b/cmd/mining/cmd/status.go @@ -2,9 +2,10 @@ package cmd import ( "fmt" - "strings" "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) // statusCmd represents the status command @@ -27,7 +28,7 @@ var statusCmd = &cobra.Command{ return fmt.Errorf("failed to get miner stats: %w", err) } - fmt.Printf("Miner Status for %s:\n", strings.Title(minerName)) + fmt.Printf("Miner Status for %s:\n", cases.Title(language.English).String(minerName)) fmt.Printf(" Hash Rate: %d H/s\n", stats.Hashrate) fmt.Printf(" Shares: %d\n", stats.Shares) fmt.Printf(" Rejected: %d\n", stats.Rejected) diff --git a/mkdocs.yml b/mkdocs.yml index bf310bf..a8b7dee 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -139,94 +139,33 @@ nav: - Home: index.md - Getting Started: - - Introduction: getting-started/introduction.md - - Installation: getting-started/installation.md - - Quick Start: getting-started/quickstart.md - - Configuration: getting-started/configuration.md - - First Mining Session: getting-started/first-session.md + - Installation: getting-started/index.md + - Quick Start: getting-started/quick-start.md - - CLI Reference: - - Overview: cli/overview.md - - serve: cli/serve.md - - start: cli/start.md - - stop: cli/stop.md - - status: cli/status.md - - install: cli/install.md - - uninstall: cli/uninstall.md - - update: cli/update.md - - list: cli/list.md - - doctor: cli/doctor.md - - profiles: cli/profiles.md + - User Guide: + - CLI: user-guide/cli.md + - Web Dashboard: user-guide/web-dashboard.md + - Desktop App: user-guide/desktop-app.md - API Reference: - - Overview: api/overview.md - - Authentication: api/authentication.md - - System Endpoints: api/system.md - - Miner Endpoints: api/miners.md - - Profile Endpoints: api/profiles.md - - Stats Endpoints: api/stats.md - - Swagger UI: api/swagger.md - - Error Handling: api/errors.md + - Overview: api/index.md + - Endpoints: api/endpoints.md + - Swagger Docs: swagger.json - - Web Dashboard: - - Overview: dashboard/overview.md - - Installation: dashboard/installation.md - - Web Component: dashboard/web-component.md - - Features: dashboard/features.md - - Configuration: dashboard/configuration.md - - Customization: dashboard/customization.md - - - Desktop Application: - - Overview: desktop/overview.md - - Installation: desktop/installation.md - - Building from Source: desktop/building.md - - Platform Support: desktop/platforms.md - - Configuration: desktop/configuration.md - - Troubleshooting: desktop/troubleshooting.md - - - Development Guide: - - Setup: development/setup.md - - Project Structure: development/structure.md - - Building: development/building.md - - Testing: development/testing.md - - E2E Tests: development/e2e-tests.md - - Code Style: development/code-style.md + - Development: + - Setup: development/index.md + - Architecture: development/architecture.md - Contributing: development/contributing.md - - Release Process: development/releases.md - - Architecture: - - Overview: architecture/overview.md - - Go Backend: architecture/backend.md - - Miner Interface: architecture/miner-interface.md - - Manager System: architecture/manager.md - - REST Service: architecture/rest-service.md - - XMRig Implementation: architecture/xmrig.md - - Profile System: architecture/profiles.md - - Stats Collection: architecture/stats.md - - XDG Directories: architecture/xdg.md + - Reference: + - Mining Pools: reference/pools.md + - Algorithms: reference/algorithms.md - - Pool Integration: - - Research Overview: pools/research.md - - Supported Algorithms: pools/algorithms.md - - Stratum Protocol: pools/stratum.md - - Pool Configuration: pools/configuration.md - - ETChash Pools: pools/etchash.md - - ProgPowZ Pools: pools/progpowz.md - - Blake3DCR Pools: pools/blake3dcr.md - - Custom Pools: pools/custom.md - - - Miners: - - Overview: miners/overview.md - - XMRig: miners/xmrig.md - - Adding New Miners: miners/adding-miners.md - - - Troubleshooting: - - Common Issues: troubleshooting/common-issues.md - - Logs: troubleshooting/logs.md - - Performance: troubleshooting/performance.md - - GPU Issues: troubleshooting/gpu.md - - Network Issues: troubleshooting/network.md - - - FAQ: faq.md - - Changelog: changelog.md - - License: license.md + - Legacy Docs: + - Start Here: 00-START-HERE.md + - Quick Reference: QUICK-REFERENCE.md + - Pool Research: pool-research.md + - Pool Integration: pool-integration-guide.md + - Pool README: POOL-RESEARCH-README.md + - Files Index: FILES-INDEX.md + - Manifest: MANIFEST.md diff --git a/pkg/mining/manager.go b/pkg/mining/manager.go index 2b35d7c..5553980 100644 --- a/pkg/mining/manager.go +++ b/pkg/mining/manager.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "net" + "regexp" "strconv" "strings" "sync" @@ -12,6 +13,9 @@ import ( "github.com/Snider/Mining/pkg/database" ) +// sanitizeInstanceName ensures the instance name only contains safe characters. +var instanceNameRegex = regexp.MustCompile(`[^a-zA-Z0-9_/-]`) + // ManagerInterface defines the contract for a miner manager. type ManagerInterface interface { StartMiner(minerType string, config *Config) (Miner, error) @@ -203,7 +207,9 @@ func (m *Manager) StartMiner(minerType string, config *Config) (Miner, error) { instanceName := miner.GetName() if config.Algo != "" { - instanceName = fmt.Sprintf("%s-%s", instanceName, config.Algo) + // Sanitize algo to prevent directory traversal or invalid filenames + sanitizedAlgo := instanceNameRegex.ReplaceAllString(config.Algo, "_") + instanceName = fmt.Sprintf("%s-%s", instanceName, sanitizedAlgo) } else { instanceName = fmt.Sprintf("%s-%d", instanceName, time.Now().UnixNano()%1000) } diff --git a/pkg/mining/miner.go b/pkg/mining/miner.go index e7bcef2..a0d7033 100644 --- a/pkg/mining/miner.go +++ b/pkg/mining/miner.go @@ -18,6 +18,7 @@ import ( "strconv" "strings" "sync" + "syscall" "time" "github.com/adrg/xdg" @@ -127,7 +128,8 @@ func (b *BaseMiner) GetBinaryPath() string { return b.MinerBinary } -// Stop terminates the miner process. +// Stop terminates the miner process gracefully. +// It first tries SIGTERM to allow cleanup, then SIGKILL if needed. func (b *BaseMiner) Stop() error { b.mu.Lock() defer b.mu.Unlock() @@ -142,6 +144,25 @@ func (b *BaseMiner) Stop() error { b.stdinPipe = nil } + // Try graceful shutdown with SIGTERM first (Unix only) + if runtime.GOOS != "windows" { + if err := b.cmd.Process.Signal(syscall.SIGTERM); err == nil { + // Wait up to 3 seconds for graceful shutdown + done := make(chan error, 1) + go func() { + _, err := b.cmd.Process.Wait() + done <- err + }() + + select { + case <-done: + return nil + case <-time.After(3 * time.Second): + // Process didn't exit, force kill + } + } + } + return b.cmd.Process.Kill() } @@ -371,10 +392,14 @@ func (b *BaseMiner) GetLowResHistoryLength() int { // GetLogs returns the captured log output from the miner process. func (b *BaseMiner) GetLogs() []string { - if b.LogBuffer == nil { + b.mu.RLock() + logBuffer := b.LogBuffer + b.mu.RUnlock() + + if logBuffer == nil { return []string{} } - return b.LogBuffer.GetLines() + return logBuffer.GetLines() } // ReduceHashrateHistory aggregates and trims hashrate data. diff --git a/pkg/mining/service.go b/pkg/mining/service.go index f5e33ee..fafe1d4 100644 --- a/pkg/mining/service.go +++ b/pkg/mining/service.go @@ -109,9 +109,13 @@ func (s *Service) ServiceStartup(ctx context.Context) error { s.InitRouter() s.Server.Handler = s.Router + // Channel to capture server startup errors + errChan := make(chan error, 1) + go func() { if err := s.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("could not listen on %s: %v\n", s.Server.Addr, err) + log.Printf("Server error on %s: %v", s.Server.Addr, err) + errChan <- err } }() @@ -121,11 +125,18 @@ func (s *Service) ServiceStartup(ctx context.Context) error { ctxShutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := s.Server.Shutdown(ctxShutdown); err != nil { - log.Fatalf("server shutdown failed: %+v", err) + log.Printf("Server shutdown error: %v", err) } }() - return nil + // Give the server a moment to start and check for immediate errors + select { + case err := <-errChan: + return fmt.Errorf("failed to start server: %w", err) + case <-time.After(100 * time.Millisecond): + // Server started successfully + return nil + } } // SetupRoutes configures all API routes on the Gin router. @@ -230,7 +241,10 @@ func (s *Service) updateInstallationCache() (*SystemInfo, error) { default: continue } - details, _ := miner.CheckInstallation() + details, err := miner.CheckInstallation() + if err != nil { + log.Printf("Warning: failed to check installation for %s: %v", availableMiner.Name, err) + } systemInfo.InstalledMinersInfo = append(systemInfo.InstalledMinersInfo, details) }