Mining/cmd/mining/cmd/remote.go
Virgil 68c826a3d8
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run
Align mining AX naming and comments
2026-04-04 05:33:08 +00:00

412 lines
12 KiB
Go

package cmd
import (
"fmt"
"strings"
"sync"
"time"
"forge.lthn.ai/Snider/Mining/pkg/node"
"github.com/spf13/cobra"
)
var (
remoteController *node.Controller
peerTransport *node.Transport
remoteControllerOnce sync.Once
remoteControllerErr error
)
// remote status, remote start, remote stop, remote logs, remote connect, remote disconnect, and remote ping live under this command group.
var remoteCmd = &cobra.Command{
Use: "remote",
Short: "Control remote mining nodes",
Long: `Send commands to remote worker nodes and retrieve their status.`,
}
// remote status a1b2c3d4e5f6 prints stats for one peer, while `remote status` prints the whole fleet.
var remoteStatusCmd = &cobra.Command{
Use: "status [peer-id]",
Short: "Get mining status from remote peers",
Long: `Display mining statistics from all connected peers or a specific peer.`,
RunE: func(cmd *cobra.Command, args []string) error {
remoteController, err := getController()
if err != nil {
return err
}
if len(args) > 0 {
// Get stats from specific peer
peerID := args[0]
peer := findPeerByPartialID(peerID)
if peer == nil {
return fmt.Errorf("peer not found: %s", peerID)
}
stats, err := remoteController.GetRemoteStats(peer.ID)
if err != nil {
return fmt.Errorf("failed to get stats: %w", err)
}
printPeerStats(peer, stats)
} else {
// Get stats from all peers
allStats := remoteController.GetAllStats()
if len(allStats) == 0 {
fmt.Println("No connected peers.")
return nil
}
peerRegistry, _ := getPeerRegistry()
var totalHashrate float64
for peerID, stats := range allStats {
peer := peerRegistry.GetPeer(peerID)
if peer != nil {
printPeerStats(peer, stats)
for _, miner := range stats.Miners {
totalHashrate += miner.Hashrate
}
}
}
fmt.Println("────────────────────────────────────")
fmt.Printf("Total Fleet Hashrate: %.2f H/s\n", totalHashrate)
}
return nil
},
}
// remote start a1b2c3d4e5f6 --type xmrig --profile default starts a miner on the selected peer.
var remoteStartCmd = &cobra.Command{
Use: "start <peer-id>",
Short: "Start miner on remote peer",
Long: `Start a miner on a remote peer using a profile.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
minerType, _ := cmd.Flags().GetString("type")
if minerType == "" {
return fmt.Errorf("--type is required, for example `xmrig` or `tt-miner`")
}
profileID, _ := cmd.Flags().GetString("profile")
peerID := args[0]
peer := findPeerByPartialID(peerID)
if peer == nil {
return fmt.Errorf("peer not found: %s", peerID)
}
remoteController, err := getController()
if err != nil {
return err
}
fmt.Printf("Starting %s miner on %s with profile %s...\n", minerType, peer.Name, profileID)
if err := remoteController.StartRemoteMiner(peer.ID, minerType, profileID, nil); err != nil {
return fmt.Errorf("failed to start miner: %w", err)
}
fmt.Println("Miner started successfully.")
return nil
},
}
// remote stop a1b2c3d4e5f6 xmrig-1 stops a named miner on the selected peer.
var remoteStopCmd = &cobra.Command{
Use: "stop <peer-id> [miner-name]",
Short: "Stop miner on remote peer",
Long: `Stop a running miner on a remote peer.`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
peerID := args[0]
peer := findPeerByPartialID(peerID)
if peer == nil {
return fmt.Errorf("peer not found: %s", peerID)
}
minerName := ""
if len(args) > 1 {
minerName = args[1]
} else {
minerName, _ = cmd.Flags().GetString("miner")
}
if minerName == "" {
return fmt.Errorf("miner name required (as argument or --miner flag)")
}
remoteController, err := getController()
if err != nil {
return err
}
fmt.Printf("Stopping miner %s on %s...\n", minerName, peer.Name)
if err := remoteController.StopRemoteMiner(peer.ID, minerName); err != nil {
return fmt.Errorf("failed to stop miner: %w", err)
}
fmt.Println("Miner stopped successfully.")
return nil
},
}
// remote logs a1b2c3d4e5f6 xmrig-1 prints the first 100 log lines for the remote miner.
var remoteLogsCmd = &cobra.Command{
Use: "logs <peer-id> <miner-name>",
Short: "Get console logs from remote miner",
Long: `Retrieve console output logs from a miner running on a remote peer.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
peerID := args[0]
minerName := args[1]
lines, _ := cmd.Flags().GetInt("lines")
peer := findPeerByPartialID(peerID)
if peer == nil {
return fmt.Errorf("peer not found: %s", peerID)
}
remoteController, err := getController()
if err != nil {
return err
}
logLines, err := remoteController.GetRemoteLogs(peer.ID, minerName, lines)
if err != nil {
return fmt.Errorf("failed to get logs: %w", err)
}
fmt.Printf("Logs from %s on %s (%d lines):\n", minerName, peer.Name, len(logLines))
fmt.Println("────────────────────────────────────")
for _, line := range logLines {
fmt.Println(line)
}
return nil
},
}
// remote connect a1b2c3d4e5f6 opens a WebSocket connection to the peer.
var remoteConnectCmd = &cobra.Command{
Use: "connect <peer-id>",
Short: "Connect to a remote peer",
Long: `Establish a WebSocket connection to a registered peer.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
peerID := args[0]
peer := findPeerByPartialID(peerID)
if peer == nil {
return fmt.Errorf("peer not found: %s", peerID)
}
remoteController, err := getController()
if err != nil {
return err
}
fmt.Printf("Connecting to %s at %s...\n", peer.Name, peer.Address)
if err := remoteController.ConnectToPeer(peer.ID); err != nil {
return fmt.Errorf("failed to connect: %w", err)
}
fmt.Println("Connected successfully.")
return nil
},
}
// remote disconnect a1b2c3d4e5f6 closes the active peer connection.
var remoteDisconnectCmd = &cobra.Command{
Use: "disconnect <peer-id>",
Short: "Disconnect from a remote peer",
Long: `Close the connection to a peer.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
peerID := args[0]
peer := findPeerByPartialID(peerID)
if peer == nil {
return fmt.Errorf("peer not found: %s", peerID)
}
remoteController, err := getController()
if err != nil {
return err
}
fmt.Printf("Disconnecting from %s...\n", peer.Name)
if err := remoteController.DisconnectFromPeer(peer.ID); err != nil {
return fmt.Errorf("failed to disconnect: %w", err)
}
fmt.Println("Disconnected.")
return nil
},
}
// remote ping a1b2c3d4e5f6 --count 4 averages four ping samples.
var remotePingCmd = &cobra.Command{
Use: "ping <peer-id>",
Short: "Ping a remote peer",
Long: `Send a ping to a peer and measure round-trip latency.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
count, _ := cmd.Flags().GetInt("count")
peerID := args[0]
peer := findPeerByPartialID(peerID)
if peer == nil {
return fmt.Errorf("peer not found: %s", peerID)
}
remoteController, err := getController()
if err != nil {
return err
}
fmt.Printf("Pinging %s (%s)...\n", peer.Name, peer.Address)
var totalRoundTripMillis float64
var successfulPings int
for i := 0; i < count; i++ {
rtt, err := remoteController.PingPeer(peer.ID)
if err != nil {
fmt.Printf(" Ping %d: timeout\n", i+1)
continue
}
fmt.Printf(" Ping %d: %.2f ms\n", i+1, rtt)
totalRoundTripMillis += rtt
successfulPings++
if i < count-1 {
time.Sleep(time.Second)
}
}
if successfulPings > 0 {
fmt.Printf("\nAverage: %.2f ms (%d/%d successful)\n", totalRoundTripMillis/float64(successfulPings), successfulPings, count)
} else {
fmt.Println("\nAll pings failed.")
}
return nil
},
}
func init() {
rootCmd.AddCommand(remoteCmd)
// remoteCmd.AddCommand(remoteStatusCmd) // exposes `remote status <peer-id>`
remoteCmd.AddCommand(remoteStatusCmd)
// remoteCmd.AddCommand(remoteStartCmd) // exposes `remote start <peer-id> --type xmrig --profile default`
remoteCmd.AddCommand(remoteStartCmd)
remoteStartCmd.Flags().StringP("profile", "p", "", "Profile ID to use for starting the miner")
remoteStartCmd.Flags().StringP("type", "t", "", "Miner type, for example xmrig or tt-miner")
// remoteCmd.AddCommand(remoteStopCmd) // exposes `remote stop <peer-id> --miner xmrig-1`
remoteCmd.AddCommand(remoteStopCmd)
remoteStopCmd.Flags().StringP("miner", "m", "", "Miner name to stop")
// remoteCmd.AddCommand(remoteLogsCmd) // exposes `remote logs <peer-id> <miner-name>`
remoteCmd.AddCommand(remoteLogsCmd)
remoteLogsCmd.Flags().IntP("lines", "n", 100, "Number of log lines to retrieve")
// remoteCmd.AddCommand(remoteConnectCmd) // exposes `remote connect <peer-id>`
remoteCmd.AddCommand(remoteConnectCmd)
// remoteCmd.AddCommand(remoteDisconnectCmd) // exposes `remote disconnect <peer-id>`
remoteCmd.AddCommand(remoteDisconnectCmd)
// remoteCmd.AddCommand(remotePingCmd) // exposes `remote ping <peer-id>`
remoteCmd.AddCommand(remotePingCmd)
remotePingCmd.Flags().IntP("count", "c", 4, "Number of pings to send")
}
// getController returns or creates the controller instance (thread-safe).
func getController() (*node.Controller, error) {
remoteControllerOnce.Do(func() {
nodeManager, err := getNodeManager()
if err != nil {
remoteControllerErr = fmt.Errorf("failed to get node manager: %w", err)
return
}
if !nodeManager.HasIdentity() {
remoteControllerErr = fmt.Errorf("no node identity found. Run `node init` first")
return
}
peerRegistry, err := getPeerRegistry()
if err != nil {
remoteControllerErr = fmt.Errorf("failed to get peer registry: %w", err)
return
}
transportConfig := node.DefaultTransportConfig()
peerTransport = node.NewTransport(nodeManager, peerRegistry, transportConfig)
remoteController = node.NewController(nodeManager, peerRegistry, peerTransport)
})
return remoteController, remoteControllerErr
}
// findPeerByPartialID("a1b2c3") returns the peer whose ID starts with `a1b2c3`.
func findPeerByPartialID(partialID string) *node.Peer {
peerRegistry, err := getPeerRegistry()
if err != nil {
return nil
}
// peerRegistry.GetPeer(partialID) tries the exact peer ID first.
peer := peerRegistry.GetPeer(partialID)
if peer != nil {
return peer
}
// peerRegistry.ListPeers() falls back to partial IDs such as `a1b2c3`.
for _, p := range peerRegistry.ListPeers() {
if strings.HasPrefix(p.ID, partialID) {
return p
}
// Also try matching by name
if strings.EqualFold(p.Name, partialID) {
return p
}
}
return nil
}
// printPeerStats(peer, stats) formats the remote stats output for `remote status`.
func printPeerStats(peer *node.Peer, stats *node.StatsPayload) {
fmt.Printf("\n%s (%s)\n", peer.Name, peer.ID[:16])
fmt.Printf(" Address: %s\n", peer.Address)
fmt.Printf(" Uptime: %s\n", formatDuration(time.Duration(stats.Uptime)*time.Second))
fmt.Printf(" Miners: %d\n", len(stats.Miners))
if len(stats.Miners) > 0 {
fmt.Println()
for _, miner := range stats.Miners {
fmt.Printf(" %s (%s)\n", miner.Name, miner.Type)
fmt.Printf(" Hashrate: %.2f H/s\n", miner.Hashrate)
fmt.Printf(" Shares: %d (rejected: %d)\n", miner.Shares, miner.Rejected)
fmt.Printf(" Algorithm: %s\n", miner.Algorithm)
fmt.Printf(" Pool: %s\n", miner.Pool)
}
}
}
// formatDuration(90*time.Minute) // returns "1h 30m"
func formatDuration(d time.Duration) string {
days := int(d.Hours() / 24)
hours := int(d.Hours()) % 24
minutes := int(d.Minutes()) % 60
if days > 0 {
return fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
}
if hours > 0 {
return fmt.Sprintf("%dh %dm", hours, minutes)
}
return fmt.Sprintf("%dm", minutes)
}