Corrected the build script in `ui/package.json` to correctly bundle the Angular application. Also updated `pkg/mining/service.go` to serve the correct bundled JavaScript file. Verified the backend server is running and accessible by testing the Swagger UI endpoint.
430 lines
13 KiB
Go
430 lines
13 KiB
Go
package mining
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
"github.com/Snider/Mining/docs"
|
|
"github.com/gin-contrib/cors"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/shirou/gopsutil/v4/mem" // Import mem for memory stats
|
|
"github.com/swaggo/swag"
|
|
|
|
swaggerFiles "github.com/swaggo/files"
|
|
ginSwagger "github.com/swaggo/gin-swagger"
|
|
)
|
|
|
|
// Service encapsulates the gin-gonic router and the mining manager.
|
|
type Service struct {
|
|
Manager ManagerInterface
|
|
Router *gin.Engine
|
|
Server *http.Server
|
|
DisplayAddr string
|
|
SwaggerInstanceName string
|
|
APIBasePath string
|
|
SwaggerUIPath string
|
|
}
|
|
|
|
// NewService creates a new mining service
|
|
func NewService(manager ManagerInterface, listenAddr string, displayAddr string, swaggerNamespace string) *Service {
|
|
apiBasePath := "/" + strings.Trim(swaggerNamespace, "/")
|
|
swaggerUIPath := apiBasePath + "/swagger" // Serve Swagger UI under a distinct sub-path
|
|
|
|
// Dynamically configure Swagger at runtime
|
|
docs.SwaggerInfo.Title = "Mining Module API"
|
|
docs.SwaggerInfo.Version = "1.0"
|
|
docs.SwaggerInfo.Host = displayAddr // Use the displayable address for Swagger UI
|
|
docs.SwaggerInfo.BasePath = apiBasePath
|
|
// Use a unique instance name to avoid conflicts in a multi-module environment
|
|
instanceName := "swagger_" + strings.ReplaceAll(strings.Trim(swaggerNamespace, "/"), "/", "_")
|
|
swag.Register(instanceName, docs.SwaggerInfo)
|
|
|
|
return &Service{
|
|
Manager: manager,
|
|
Server: &http.Server{
|
|
Addr: listenAddr, // Server listens on this address
|
|
},
|
|
DisplayAddr: displayAddr, // Store displayable address for messages
|
|
SwaggerInstanceName: instanceName,
|
|
APIBasePath: apiBasePath,
|
|
SwaggerUIPath: swaggerUIPath,
|
|
}
|
|
}
|
|
|
|
func (s *Service) ServiceStartup(ctx context.Context) error {
|
|
s.Router = gin.Default()
|
|
s.Router.Use(cors.Default())
|
|
s.setupRoutes()
|
|
s.Server.Handler = s.Router
|
|
|
|
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)
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
// Stop the manager's background goroutines
|
|
s.Manager.Stop()
|
|
|
|
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)
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) setupRoutes() {
|
|
// All API routes are now relative to the service's APIBasePath
|
|
apiGroup := s.Router.Group(s.APIBasePath)
|
|
{
|
|
apiGroup.GET("/info", s.handleGetInfo) // New GET endpoint for cached info
|
|
apiGroup.POST("/doctor", s.handleDoctor)
|
|
apiGroup.POST("/update", s.handleUpdateCheck)
|
|
|
|
minersGroup := apiGroup.Group("/miners")
|
|
{
|
|
minersGroup.GET("", s.handleListMiners)
|
|
minersGroup.GET("/available", s.handleListAvailableMiners)
|
|
minersGroup.POST("/:miner_name", s.handleStartMiner)
|
|
minersGroup.POST("/:miner_name/install", s.handleInstallMiner)
|
|
minersGroup.DELETE("/:miner_name/uninstall", s.handleUninstallMiner)
|
|
minersGroup.DELETE("/:miner_name", s.handleStopMiner)
|
|
minersGroup.GET("/:miner_name/stats", s.handleGetMinerStats)
|
|
minersGroup.GET("/:miner_name/hashrate-history", s.handleGetMinerHashrateHistory) // New endpoint
|
|
}
|
|
}
|
|
|
|
// New route to serve the custom HTML element bundle
|
|
// This path now points to the output of the Angular project within the 'ui' directory
|
|
s.Router.StaticFile("/component/mining-dashboard.js", "./ui/dist/ui/mbe-mining-dashboard.js")
|
|
|
|
// Register Swagger UI route under a distinct sub-path to avoid conflicts
|
|
swaggerURL := ginSwagger.URL(fmt.Sprintf("http://%s%s/doc.json", s.DisplayAddr, s.SwaggerUIPath))
|
|
s.Router.GET(s.SwaggerUIPath+"/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, swaggerURL, ginSwagger.InstanceName(s.SwaggerInstanceName)))
|
|
}
|
|
|
|
// handleGetInfo godoc
|
|
// @Summary Get cached miner installation information
|
|
// @Description Retrieves the last cached installation details for all miners, along with system information.
|
|
// @Tags system
|
|
// @Produce json
|
|
// @Success 200 {object} SystemInfo
|
|
// @Failure 500 {object} map[string]string "Internal server error"
|
|
// @Router /info [get]
|
|
func (s *Service) handleGetInfo(c *gin.Context) {
|
|
systemInfo := SystemInfo{
|
|
Timestamp: time.Now(),
|
|
OS: runtime.GOOS,
|
|
Architecture: runtime.GOARCH,
|
|
GoVersion: runtime.Version(),
|
|
AvailableCPUCores: runtime.NumCPU(),
|
|
}
|
|
|
|
// Get total system RAM
|
|
vMem, err := mem.VirtualMemory()
|
|
if err != nil {
|
|
log.Printf("Warning: Failed to get virtual memory info: %v", err)
|
|
systemInfo.TotalSystemRAMGB = 0.0 // Default to 0 on error
|
|
} else {
|
|
// Convert bytes to GB
|
|
systemInfo.TotalSystemRAMGB = float64(vMem.Total) / (1024 * 1024 * 1024)
|
|
}
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not get home directory"})
|
|
return
|
|
}
|
|
signpostPath := filepath.Join(homeDir, ".installed-miners")
|
|
|
|
configPathBytes, err := os.ReadFile(signpostPath)
|
|
if err != nil {
|
|
// If signpost or cache doesn't exist, return SystemInfo with empty miner details
|
|
systemInfo.InstalledMinersInfo = []*InstallationDetails{}
|
|
c.JSON(http.StatusOK, systemInfo)
|
|
return
|
|
}
|
|
configPath := string(configPathBytes)
|
|
|
|
cacheBytes, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
// If cache file is missing, return SystemInfo with empty miner details
|
|
systemInfo.InstalledMinersInfo = []*InstallationDetails{}
|
|
c.JSON(http.StatusOK, systemInfo)
|
|
return
|
|
}
|
|
|
|
var cachedDetails []*InstallationDetails
|
|
if err := json.Unmarshal(cacheBytes, &cachedDetails); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not parse cache file"})
|
|
return
|
|
}
|
|
|
|
// Filter for only installed miners
|
|
var installedOnly []*InstallationDetails
|
|
for _, detail := range cachedDetails {
|
|
if detail.IsInstalled {
|
|
installedOnly = append(installedOnly, detail)
|
|
}
|
|
}
|
|
systemInfo.InstalledMinersInfo = installedOnly
|
|
|
|
c.JSON(http.StatusOK, systemInfo)
|
|
}
|
|
|
|
// handleDoctor godoc
|
|
// @Summary Check miner installations
|
|
// @Description Performs a live check on all available miners to verify their installation status, version, and path.
|
|
// @Tags system
|
|
// @Produce json
|
|
// @Success 200 {array} InstallationDetails
|
|
// @Router /doctor [post]
|
|
func (s *Service) handleDoctor(c *gin.Context) {
|
|
var allDetails []*InstallationDetails
|
|
for _, availableMiner := range s.Manager.ListAvailableMiners() {
|
|
var miner Miner
|
|
switch availableMiner.Name {
|
|
case "xmrig":
|
|
miner = NewXMRigMiner()
|
|
default:
|
|
continue
|
|
}
|
|
details, err := miner.CheckInstallation()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check " + miner.GetName(), "details": err.Error()})
|
|
return
|
|
}
|
|
allDetails = append(allDetails, details)
|
|
}
|
|
c.JSON(http.StatusOK, allDetails)
|
|
}
|
|
|
|
// handleUpdateCheck godoc
|
|
// @Summary Check for miner updates
|
|
// @Description Checks if any installed miners have a new version available for download.
|
|
// @Tags system
|
|
// @Produce json
|
|
// @Success 200 {object} map[string]string
|
|
// @Router /update [post]
|
|
func (s *Service) handleUpdateCheck(c *gin.Context) {
|
|
updates := make(map[string]string)
|
|
for _, availableMiner := range s.Manager.ListAvailableMiners() {
|
|
var miner Miner
|
|
switch availableMiner.Name {
|
|
case "xmrig":
|
|
miner = NewXMRigMiner()
|
|
default:
|
|
continue
|
|
}
|
|
|
|
details, err := miner.CheckInstallation()
|
|
if err != nil || !details.IsInstalled {
|
|
continue
|
|
}
|
|
|
|
latestVersionStr, err := miner.GetLatestVersion()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
latestVersion, err := semver.NewVersion(latestVersionStr)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
installedVersion, err := semver.NewVersion(details.Version)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if latestVersion.GreaterThan(installedVersion) {
|
|
updates[miner.GetName()] = latestVersion.String()
|
|
}
|
|
}
|
|
|
|
if len(updates) == 0 {
|
|
c.JSON(http.StatusOK, gin.H{"status": "All miners are up to date."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"updates_available": updates})
|
|
}
|
|
|
|
// handleUninstallMiner godoc
|
|
// @Summary Uninstall a miner
|
|
// @Description Removes all files for a specific miner.
|
|
// @Tags miners
|
|
// @Produce json
|
|
// @Param miner_type path string true "Miner Type to uninstall"
|
|
// @Success 200 {object} map[string]string
|
|
// @Router /miners/{miner_type}/uninstall [delete]
|
|
func (s *Service) handleUninstallMiner(c *gin.Context) {
|
|
minerType := c.Param("miner_name")
|
|
var miner Miner
|
|
switch minerType {
|
|
case "xmrig":
|
|
miner = NewXMRigMiner()
|
|
default:
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "unknown miner type"})
|
|
return
|
|
}
|
|
if err := miner.Uninstall(); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"status": miner.GetName() + " uninstalled successfully."})
|
|
}
|
|
|
|
// handleListMiners godoc
|
|
// @Summary List all running miners
|
|
// @Description Get a list of all running miners
|
|
// @Tags miners
|
|
// @Produce json
|
|
// @Success 200 {array} XMRigMiner
|
|
// @Router /miners [get]
|
|
func (s *Service) handleListMiners(c *gin.Context) {
|
|
miners := s.Manager.ListMiners()
|
|
c.JSON(http.StatusOK, miners)
|
|
}
|
|
|
|
// handleListAvailableMiners godoc
|
|
// @Summary List all available miners
|
|
// @Description Get a list of all available miners
|
|
// @Tags miners
|
|
// @Produce json
|
|
// @Success 200 {array} AvailableMiner
|
|
// @Router /miners/available [get]
|
|
func (s *Service) handleListAvailableMiners(c *gin.Context) {
|
|
miners := s.Manager.ListAvailableMiners()
|
|
c.JSON(http.StatusOK, miners)
|
|
}
|
|
|
|
// handleInstallMiner godoc
|
|
// @Summary Install or update a miner
|
|
// @Description Install a new miner or update an existing one.
|
|
// @Tags miners
|
|
// @Produce json
|
|
// @Param miner_type path string true "Miner Type to install/update"
|
|
// @Success 200 {object} map[string]string
|
|
// @Router /miners/{miner_type}/install [post]
|
|
func (s *Service) handleInstallMiner(c *gin.Context) {
|
|
minerType := c.Param("miner_name")
|
|
var miner Miner
|
|
switch minerType {
|
|
case "xmrig":
|
|
miner = NewXMRigMiner()
|
|
default:
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "unknown miner type"})
|
|
return
|
|
}
|
|
|
|
if err := miner.Install(); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
details, err := miner.CheckInstallation()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to verify installation", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "installed", "version": details.Version, "path": details.Path})
|
|
}
|
|
|
|
// handleStartMiner godoc
|
|
// @Summary Start a new miner
|
|
// @Description Start a new miner with the given configuration
|
|
// @Tags miners
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param miner_type path string true "Miner Type"
|
|
// @Param config body Config true "Miner Configuration"
|
|
// @Success 200 {object} XMRigMiner
|
|
// @Router /miners/{miner_type} [post]
|
|
func (s *Service) handleStartMiner(c *gin.Context) {
|
|
minerType := c.Param("miner_name")
|
|
var config Config
|
|
if err := c.ShouldBindJSON(&config); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
miner, err := s.Manager.StartMiner(minerType, &config)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, miner)
|
|
}
|
|
|
|
// handleStopMiner godoc
|
|
// @Summary Stop a running miner
|
|
// @Description Stop a running miner by its name
|
|
// @Tags miners
|
|
// @Produce json
|
|
// @Param miner_name path string true "Miner Name"
|
|
// @Success 200 {object} map[string]string
|
|
// @Router /miners/{miner_name} [delete]
|
|
func (s *Service) handleStopMiner(c *gin.Context) {
|
|
minerName := c.Param("miner_name")
|
|
if err := s.Manager.StopMiner(minerName); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"status": "stopped"})
|
|
}
|
|
|
|
// handleGetMinerStats godoc
|
|
// @Summary Get miner stats
|
|
// @Description Get statistics for a running miner
|
|
// @Tags miners
|
|
// @Produce json
|
|
// @Param miner_name path string true "Miner Name"
|
|
// @Success 200 {object} PerformanceMetrics
|
|
// @Router /miners/{miner_name}/stats [get]
|
|
func (s *Service) handleGetMinerStats(c *gin.Context) {
|
|
minerName := c.Param("miner_name")
|
|
miner, err := s.Manager.GetMiner(minerName)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "miner not found"})
|
|
return
|
|
}
|
|
stats, err := miner.GetStats()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, stats)
|
|
}
|
|
|
|
// handleGetMinerHashrateHistory godoc
|
|
// @Summary Get miner hashrate history
|
|
// @Description Get historical hashrate data for a running miner
|
|
// @Tags miners
|
|
// @Produce json
|
|
// @Param miner_name path string true "Miner Name"
|
|
// @Success 200 {array} HashratePoint
|
|
// @Router /miners/{miner_name}/hashrate-history [get]
|
|
func (s *Service) handleGetMinerHashrateHistory(c *gin.Context) {
|
|
minerName := c.Param("miner_name")
|
|
history, err := s.Manager.GetMinerHashrateHistory(minerName)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, history)
|
|
}
|