AX: clarify cache paths and remote command examples
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

This commit is contained in:
Virgil 2026-04-04 05:48:01 +00:00
parent ba0d61d4bc
commit 85cfb18ddd
3 changed files with 53 additions and 53 deletions

View file

@ -15,12 +15,12 @@ import (
const installationCachePointerFileName = ".installed-miners"
// validateConfigPath("/home/alice/.config/lethean-desktop/miners/config.json") returns nil.
// validateConfigPath("/tmp/config.json") rejects paths outside XDG_CONFIG_HOME.
func validateConfigPath(configPath string) error {
// validateCacheFilePath("/home/alice/.config/lethean-desktop/miners/config.json") returns nil.
// validateCacheFilePath("/tmp/config.json") rejects paths outside XDG_CONFIG_HOME.
func validateCacheFilePath(cacheFilePath string) error {
expectedBase := filepath.Join(xdg.ConfigHome, "lethean-desktop")
cleanPath := filepath.Clean(configPath)
cleanPath := filepath.Clean(cacheFilePath)
if !strings.HasPrefix(cleanPath, expectedBase+string(os.PathSeparator)) && cleanPath != expectedBase {
return fmt.Errorf("invalid config path: must be within %s", expectedBase)
@ -29,7 +29,7 @@ func validateConfigPath(configPath string) error {
return nil
}
// doctor refreshes the miner installation cache and writes the refreshed summary on disk.
// doctorCmd adds `doctor` so `mining doctor` 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",
@ -42,7 +42,7 @@ var doctorCmd = &cobra.Command{
if err := updateDoctorCache(); err != nil {
return fmt.Errorf("failed to run doctor check: %w", err)
}
// loadAndDisplayCache() prints the refreshed miner summary after updateDoctorCache().
// loadAndDisplayCache() prints the refreshed miner summary after `mining doctor` refreshes the cache.
_, err := loadAndDisplayCache()
return err
},
@ -55,30 +55,30 @@ func loadAndDisplayCache() (bool, error) {
}
signpostPath := filepath.Join(homeDir, installationCachePointerFileName)
// os.Stat(signpostPath) returns os.ErrNotExist when no cache has been written yet.
// os.Stat("/home/alice/.installed-miners") returns os.ErrNotExist before the first `mining install xmrig` run.
if _, err := os.Stat(signpostPath); os.IsNotExist(err) {
fmt.Println("No cached data found. Run 'install' for a miner first.")
return false, nil // loadAndDisplayCache returns false until install writes the first cache file.
}
configPathBytes, err := os.ReadFile(signpostPath)
cachePointerBytes, err := os.ReadFile(signpostPath)
if err != nil {
return false, fmt.Errorf("could not read signpost file: %w", err)
}
configPath := strings.TrimSpace(string(configPathBytes))
cacheFilePath := strings.TrimSpace(string(cachePointerBytes))
// validateConfigPath("/home/alice/.config/lethean-desktop/miners/config.json") blocks path traversal outside XDG_CONFIG_HOME.
if err := validateConfigPath(configPath); err != nil {
// validateCacheFilePath("/home/alice/.config/lethean-desktop/miners/config.json") blocks path traversal outside XDG_CONFIG_HOME.
if err := validateCacheFilePath(cacheFilePath); err != nil {
return false, fmt.Errorf("security error: %w", err)
}
cacheBytes, err := os.ReadFile(configPath)
cacheBytes, err := os.ReadFile(cacheFilePath)
if err != nil {
if os.IsNotExist(err) {
fmt.Println("No cached data found. Run 'install' for a miner first.")
return false, nil
}
return false, fmt.Errorf("could not read cache file from %s: %w", configPath, err)
return false, fmt.Errorf("could not read cache file from %s: %w", cacheFilePath, err)
}
var systemInfo mining.SystemInfo
@ -109,22 +109,22 @@ func loadAndDisplayCache() (bool, error) {
}
func saveResultsToCache(systemInfo *mining.SystemInfo) error {
cacheRelativePath := filepath.Join("lethean-desktop", "miners")
configDir, err := xdg.ConfigFile(cacheRelativePath)
cacheDirectoryRelativePath := filepath.Join("lethean-desktop", "miners")
cacheDirectoryPath, err := xdg.ConfigFile(cacheDirectoryRelativePath)
if err != nil {
return fmt.Errorf("could not get config directory: %w", err)
}
if err := os.MkdirAll(configDir, 0755); err != nil {
if err := os.MkdirAll(cacheDirectoryPath, 0755); err != nil {
return fmt.Errorf("could not create config directory: %w", err)
}
configPath := filepath.Join(configDir, "config.json")
cacheFilePath := filepath.Join(cacheDirectoryPath, "config.json")
data, err := json.MarshalIndent(systemInfo, "", " ")
if err != nil {
return fmt.Errorf("could not marshal cache data: %w", err)
}
if err := os.WriteFile(configPath, data, 0600); err != nil {
if err := os.WriteFile(cacheFilePath, data, 0600); err != nil {
return fmt.Errorf("could not write cache file: %w", err)
}
@ -133,11 +133,11 @@ func saveResultsToCache(systemInfo *mining.SystemInfo) error {
return fmt.Errorf("could not get home directory for signpost: %w", err)
}
signpostPath := filepath.Join(homeDir, installationCachePointerFileName)
if err := os.WriteFile(signpostPath, []byte(configPath), 0600); err != nil {
if err := os.WriteFile(signpostPath, []byte(cacheFilePath), 0600); err != nil {
return fmt.Errorf("could not write signpost file: %w", err)
}
fmt.Printf("\n(Cache updated at %s)\n", configPath)
fmt.Printf("\n(Cache updated at %s)\n", cacheFilePath)
return nil
}

View file

@ -17,14 +17,14 @@ var (
remoteControllerErr error
)
// remote status, remote start, remote stop, remote logs, remote connect, remote disconnect, and remote ping live under this command group.
// remote status peer-19f3, remote start peer-19f3 --type xmrig, and remote ping peer-19f3 --count 4 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.
// remote status peer-19f3 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",
@ -36,7 +36,7 @@ var remoteStatusCmd = &cobra.Command{
}
if len(args) > 0 {
// Get stats from specific peer
// remote status peer-19f3 shows that peer's stats.
peerID := args[0]
peer := findPeerByPartialID(peerID)
if peer == nil {
@ -50,7 +50,7 @@ var remoteStatusCmd = &cobra.Command{
printPeerStats(peer, stats)
} else {
// Get stats from all peers
// remote status peer-19f3 shows one peer, while `remote status` shows the fleet.
allStats := remoteController.GetAllStats()
if len(allStats) == 0 {
fmt.Println("No connected peers.")
@ -78,7 +78,7 @@ var remoteStatusCmd = &cobra.Command{
},
}
// remote start a1b2c3d4e5f6 --type xmrig --profile default starts a miner on the selected peer.
// remote start peer-19f3 --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",
@ -112,7 +112,7 @@ var remoteStartCmd = &cobra.Command{
},
}
// remote stop a1b2c3d4e5f6 xmrig-1 stops a named miner on the selected peer.
// remote stop peer-19f3 xmrig-main stops a named miner on the selected peer.
var remoteStopCmd = &cobra.Command{
Use: "stop <peer-id> [miner-name]",
Short: "Stop miner on remote peer",
@ -151,7 +151,7 @@ var remoteStopCmd = &cobra.Command{
},
}
// remote logs a1b2c3d4e5f6 xmrig-1 prints the first 100 log lines for the remote miner.
// remote logs peer-19f3 xmrig-main 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",
@ -187,7 +187,7 @@ var remoteLogsCmd = &cobra.Command{
},
}
// remote connect a1b2c3d4e5f6 opens a WebSocket connection to the peer.
// remote connect peer-19f3 opens a WebSocket connection to the peer.
var remoteConnectCmd = &cobra.Command{
Use: "connect <peer-id>",
Short: "Connect to a remote peer",
@ -215,7 +215,7 @@ var remoteConnectCmd = &cobra.Command{
},
}
// remote disconnect a1b2c3d4e5f6 closes the active peer connection.
// remote disconnect peer-19f3 closes the active peer connection.
var remoteDisconnectCmd = &cobra.Command{
Use: "disconnect <peer-id>",
Short: "Disconnect from a remote peer",
@ -243,7 +243,7 @@ var remoteDisconnectCmd = &cobra.Command{
},
}
// remote ping a1b2c3d4e5f6 --count 4 averages four ping samples.
// remote ping peer-19f3 --count 4 averages four ping samples.
var remotePingCmd = &cobra.Command{
Use: "ping <peer-id>",
Short: "Ping a remote peer",
@ -296,34 +296,34 @@ var remotePingCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(remoteCmd)
// remoteCmd.AddCommand(remoteStatusCmd) // exposes `remote status <peer-id>`
// remoteCmd.AddCommand(remoteStatusCmd) // remote status peer-19f3 prints one peer, while `remote status` prints the fleet.
remoteCmd.AddCommand(remoteStatusCmd)
// remoteCmd.AddCommand(remoteStartCmd) // exposes `remote start <peer-id> --type xmrig --profile default`
// remoteCmd.AddCommand(remoteStartCmd) // remote start peer-19f3 --type xmrig --profile default launches a miner.
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) // remote stop peer-19f3 xmrig-main stops the selected miner.
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) // remote logs peer-19f3 xmrig-main prints miner logs.
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) // remote connect peer-19f3 opens the peer connection.
remoteCmd.AddCommand(remoteConnectCmd)
// remoteCmd.AddCommand(remoteDisconnectCmd) // exposes `remote disconnect <peer-id>`
// remoteCmd.AddCommand(remoteDisconnectCmd) // remote disconnect peer-19f3 closes the peer connection.
remoteCmd.AddCommand(remoteDisconnectCmd)
// remoteCmd.AddCommand(remotePingCmd) // exposes `remote ping <peer-id>`
// remoteCmd.AddCommand(remotePingCmd) // remote ping peer-19f3 --count 4 measures latency.
remoteCmd.AddCommand(remotePingCmd)
remotePingCmd.Flags().IntP("count", "c", 4, "Number of pings to send")
}
// getController returns or creates the controller instance (thread-safe).
// getController() returns the cached controller after `node init` succeeds.
func getController() (*node.Controller, error) {
remoteControllerOnce.Do(func() {
nodeManager, err := getNodeManager()
@ -350,20 +350,20 @@ func getController() (*node.Controller, error) {
return remoteController, remoteControllerErr
}
// findPeerByPartialID("a1b2c3") returns the peer whose ID starts with `a1b2c3`.
// findPeerByPartialID("peer-19f3") returns the peer whose ID starts with `peer-19f3`.
func findPeerByPartialID(partialID string) *node.Peer {
peerRegistry, err := getPeerRegistry()
if err != nil {
return nil
}
// peerRegistry.GetPeer(partialID) tries the exact peer ID first.
// peerRegistry.GetPeer("peer-19f3") 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`.
// peerRegistry.ListPeers() falls back to partial IDs such as `peer-19`.
for _, p := range peerRegistry.ListPeers() {
if strings.HasPrefix(p.ID, partialID) {
return p
@ -377,7 +377,7 @@ func findPeerByPartialID(partialID string) *node.Peer {
return nil
}
// printPeerStats(peer, stats) formats the remote stats output for `remote status`.
// printPeerStats(peer, stats) formats the remote stats output for `remote status peer-19f3`.
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)

View file

@ -13,17 +13,17 @@ import (
"github.com/spf13/cobra"
)
// validateUpdateConfigPath("/home/alice/.config/lethean-desktop/miners/config.json") // nil
func validateUpdateConfigPath(configPath string) error {
// validateUpdateCacheFilePath("/home/alice/.config/lethean-desktop/miners/config.json") returns nil.
func validateUpdateCacheFilePath(cacheFilePath string) error {
expectedBase := filepath.Join(xdg.ConfigHome, "lethean-desktop")
cleanPath := filepath.Clean(configPath)
cleanPath := filepath.Clean(cacheFilePath)
if !strings.HasPrefix(cleanPath, expectedBase+string(os.PathSeparator)) && cleanPath != expectedBase {
return fmt.Errorf("invalid config path: must be within %s", expectedBase)
}
return nil
}
// mining update checks cached miner versions and reports available upgrades.
// updateCmd adds `update` so `mining update` can compare the cached miner version against the latest release.
var updateCmd = &cobra.Command{
Use: "update",
Short: "Check for updates to installed miners",
@ -42,23 +42,23 @@ var updateCmd = &cobra.Command{
return nil
}
configPathBytes, err := os.ReadFile(signpostPath)
cachePointerBytes, err := os.ReadFile(signpostPath)
if err != nil {
return fmt.Errorf("could not read signpost file: %w", err)
}
configPath := strings.TrimSpace(string(configPathBytes))
cacheFilePath := strings.TrimSpace(string(cachePointerBytes))
// validateUpdateConfigPath("/home/alice/.config/lethean-desktop/miners/config.json") // blocks path traversal
if err := validateUpdateConfigPath(configPath); err != nil {
// validateUpdateCacheFilePath("/home/alice/.config/lethean-desktop/miners/config.json") blocks path traversal.
if err := validateUpdateCacheFilePath(cacheFilePath); err != nil {
return fmt.Errorf("security error: %w", err)
}
cacheBytes, err := os.ReadFile(configPath)
cacheBytes, err := os.ReadFile(cacheFilePath)
if err != nil {
return fmt.Errorf("could not read cache file from %s: %w", configPath, err)
return fmt.Errorf("could not read cache file from %s: %w", cacheFilePath, err)
}
// mining.SystemInfo{} // matches what doctor.go writes to the cache file
// mining.SystemInfo{} matches the JSON shape that `mining doctor` writes to /home/alice/.config/lethean-desktop/miners/config.json.
var systemInfo mining.SystemInfo
if err := json.Unmarshal(cacheBytes, &systemInfo); err != nil {
return fmt.Errorf("could not parse cache file: %w", err)