412 lines
12 KiB
Go
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)
|
|
}
|