AX: clarify mining cache names and examples
This commit is contained in:
parent
262987c10a
commit
0893b0ef9e
5 changed files with 62 additions and 57 deletions
|
|
@ -13,11 +13,14 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const installationCachePointerFileName = ".installed-miners"
|
||||
const (
|
||||
installedMinersCachePointerFileName = ".installed-miners"
|
||||
installedMinersCacheFileName = "installed-miners.json"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
// validateInstalledMinerCachePath("/home/alice/.config/lethean-desktop/miners/installed-miners.json") returns nil.
|
||||
// validateInstalledMinerCachePath("/tmp/installed-miners.json") rejects paths outside XDG_CONFIG_HOME.
|
||||
func validateInstalledMinerCachePath(cacheFilePath string) error {
|
||||
expectedBase := filepath.Join(xdg.ConfigHome, "lethean-desktop")
|
||||
|
||||
cleanPath := filepath.Clean(cacheFilePath)
|
||||
|
|
@ -42,23 +45,23 @@ 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 `mining doctor` refreshes the cache.
|
||||
_, err := loadAndDisplayCache()
|
||||
// loadAndDisplayInstalledMinerCache() prints the refreshed miner summary after `mining doctor` refreshes the cache.
|
||||
_, err := loadAndDisplayInstalledMinerCache()
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
func loadAndDisplayCache() (bool, error) {
|
||||
func loadAndDisplayInstalledMinerCache() (bool, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("could not get home directory: %w", err)
|
||||
}
|
||||
signpostPath := filepath.Join(homeDir, installationCachePointerFileName)
|
||||
signpostPath := filepath.Join(homeDir, installedMinersCachePointerFileName)
|
||||
|
||||
// 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.
|
||||
return false, nil // loadAndDisplayInstalledMinerCache returns false until install writes the first cache file.
|
||||
}
|
||||
|
||||
cachePointerBytes, err := os.ReadFile(signpostPath)
|
||||
|
|
@ -67,8 +70,8 @@ func loadAndDisplayCache() (bool, error) {
|
|||
}
|
||||
cacheFilePath := strings.TrimSpace(string(cachePointerBytes))
|
||||
|
||||
// validateCacheFilePath("/home/alice/.config/lethean-desktop/miners/config.json") blocks path traversal outside XDG_CONFIG_HOME.
|
||||
if err := validateCacheFilePath(cacheFilePath); err != nil {
|
||||
// validateInstalledMinerCachePath("/home/alice/.config/lethean-desktop/miners/installed-miners.json") blocks path traversal outside XDG_CONFIG_HOME.
|
||||
if err := validateInstalledMinerCachePath(cacheFilePath); err != nil {
|
||||
return false, fmt.Errorf("security error: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -102,13 +105,13 @@ func loadAndDisplayCache() (bool, error) {
|
|||
} else {
|
||||
minerName = "Unknown Miner"
|
||||
}
|
||||
displayDetails(minerName, details)
|
||||
displayInstalledMinerDetails(minerName, details)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func saveResultsToCache(systemInfo *mining.SystemInfo) error {
|
||||
func saveInstalledMinerCache(systemInfo *mining.SystemInfo) error {
|
||||
cacheDirectoryRelativePath := filepath.Join("lethean-desktop", "miners")
|
||||
cacheDirectoryPath, err := xdg.ConfigFile(cacheDirectoryRelativePath)
|
||||
if err != nil {
|
||||
|
|
@ -117,7 +120,7 @@ func saveResultsToCache(systemInfo *mining.SystemInfo) error {
|
|||
if err := os.MkdirAll(cacheDirectoryPath, 0755); err != nil {
|
||||
return fmt.Errorf("could not create config directory: %w", err)
|
||||
}
|
||||
cacheFilePath := filepath.Join(cacheDirectoryPath, "config.json")
|
||||
cacheFilePath := filepath.Join(cacheDirectoryPath, installedMinersCacheFileName)
|
||||
|
||||
data, err := json.MarshalIndent(systemInfo, "", " ")
|
||||
if err != nil {
|
||||
|
|
@ -132,7 +135,7 @@ func saveResultsToCache(systemInfo *mining.SystemInfo) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("could not get home directory for signpost: %w", err)
|
||||
}
|
||||
signpostPath := filepath.Join(homeDir, installationCachePointerFileName)
|
||||
signpostPath := filepath.Join(homeDir, installedMinersCachePointerFileName)
|
||||
if err := os.WriteFile(signpostPath, []byte(cacheFilePath), 0600); err != nil {
|
||||
return fmt.Errorf("could not write signpost file: %w", err)
|
||||
}
|
||||
|
|
@ -141,7 +144,7 @@ func saveResultsToCache(systemInfo *mining.SystemInfo) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func displayDetails(minerName string, details *mining.InstallationDetails) {
|
||||
func displayInstalledMinerDetails(minerName string, details *mining.InstallationDetails) {
|
||||
fmt.Printf("--- %s ---\n", minerName)
|
||||
if details.IsInstalled {
|
||||
fmt.Printf(" Status: Installed\n")
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ func updateDoctorCache() error {
|
|||
InstalledMinersInfo: allDetails,
|
||||
}
|
||||
|
||||
return saveResultsToCache(systemInfo)
|
||||
return saveInstalledMinerCache(systemInfo)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// validateUpdateCacheFilePath("/home/alice/.config/lethean-desktop/miners/config.json") returns nil.
|
||||
// validateUpdateCacheFilePath("/home/alice/.config/lethean-desktop/miners/installed-miners.json") returns nil.
|
||||
// validateUpdateCacheFilePath("/tmp/installed-miners.json") rejects paths outside XDG_CONFIG_HOME.
|
||||
func validateUpdateCacheFilePath(cacheFilePath string) error {
|
||||
expectedBase := filepath.Join(xdg.ConfigHome, "lethean-desktop")
|
||||
cleanPath := filepath.Clean(cacheFilePath)
|
||||
|
|
@ -35,7 +36,7 @@ var updateCmd = &cobra.Command{
|
|||
if err != nil {
|
||||
return fmt.Errorf("could not get home directory: %w", err)
|
||||
}
|
||||
signpostPath := filepath.Join(homeDir, installationCachePointerFileName)
|
||||
signpostPath := filepath.Join(homeDir, installedMinersCachePointerFileName)
|
||||
|
||||
if _, err := os.Stat(signpostPath); os.IsNotExist(err) {
|
||||
fmt.Println("No miners installed yet. Run 'doctor' or 'install' first.")
|
||||
|
|
@ -48,7 +49,7 @@ var updateCmd = &cobra.Command{
|
|||
}
|
||||
cacheFilePath := strings.TrimSpace(string(cachePointerBytes))
|
||||
|
||||
// validateUpdateCacheFilePath("/home/alice/.config/lethean-desktop/miners/config.json") blocks path traversal.
|
||||
// validateUpdateCacheFilePath("/home/alice/.config/lethean-desktop/miners/installed-miners.json") blocks path traversal outside XDG_CONFIG_HOME.
|
||||
if err := validateUpdateCacheFilePath(cacheFilePath); err != nil {
|
||||
return fmt.Errorf("security error: %w", err)
|
||||
}
|
||||
|
|
@ -58,7 +59,7 @@ var updateCmd = &cobra.Command{
|
|||
return fmt.Errorf("could not read cache file from %s: %w", cacheFilePath, err)
|
||||
}
|
||||
|
||||
// mining.SystemInfo{} matches the JSON shape that `mining doctor` writes to /home/alice/.config/lethean-desktop/miners/config.json.
|
||||
// mining.SystemInfo{} matches the JSON shape that `mining doctor` writes to /home/alice/.config/lethean-desktop/miners/installed-miners.json.
|
||||
var systemInfo mining.SystemInfo
|
||||
if err := json.Unmarshal(cacheBytes, &systemInfo); err != nil {
|
||||
return fmt.Errorf("could not parse cache file: %w", err)
|
||||
|
|
|
|||
|
|
@ -261,9 +261,11 @@ func findAvailablePort() (int, error) {
|
|||
return listener.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
||||
|
||||
// miner, err := manager.StartMiner(ctx, "xmrig", &Config{Algo: "rx/0"})
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// cancel()
|
||||
// _, err := manager.StartMiner(ctx, "xmrig", &Config{Algo: "rx/0"}) // returns context.Canceled before locking
|
||||
func (manager *Manager) StartMiner(ctx context.Context, minerType string, config *Config) (Miner, error) {
|
||||
// Check for cancellation before acquiring lock
|
||||
// ctx, cancel := context.WithCancel(context.Background()); cancel(); manager.StartMiner(ctx, "xmrig", &Config{Algo: "rx/0"}) returns context.Canceled before locking.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
|
|
@ -284,7 +286,7 @@ func (manager *Manager) StartMiner(ctx context.Context, minerType string, config
|
|||
|
||||
instanceName := miner.GetName()
|
||||
if config.Algo != "" {
|
||||
// Sanitize algo to prevent directory traversal or invalid filenames
|
||||
// sanitizedAlgo := instanceNameRegex.ReplaceAllString("rx/0", "_") // "rx_0"
|
||||
sanitizedAlgo := instanceNameRegex.ReplaceAllString(config.Algo, "_")
|
||||
instanceName = instanceName + "-" + sanitizedAlgo
|
||||
} else {
|
||||
|
|
@ -295,7 +297,7 @@ func (manager *Manager) StartMiner(ctx context.Context, minerType string, config
|
|||
return nil, ErrMinerExists(instanceName)
|
||||
}
|
||||
|
||||
// Validate user-provided HTTPPort if specified
|
||||
// config.HTTPPort = 3333 keeps the miner API on a user-supplied port between 1024 and 65535.
|
||||
if config.HTTPPort != 0 {
|
||||
if config.HTTPPort < 1024 || config.HTTPPort > 65535 {
|
||||
return nil, ErrInvalidConfig("HTTPPort must be between 1024 and 65535, got " + strconv.Itoa(config.HTTPPort))
|
||||
|
|
@ -323,13 +325,13 @@ func (manager *Manager) StartMiner(ctx context.Context, minerType string, config
|
|||
}
|
||||
}
|
||||
|
||||
// manager.emitEvent(EventMinerStarting, MinerEventData{Name: "xmrig-rx_0"}) // fires before miner.Start(config)
|
||||
// manager.emitEvent(EventMinerStarting, MinerEventData{Name: "xmrig-rx_0"}) fires before miner.Start(config).
|
||||
manager.emitEvent(EventMinerStarting, MinerEventData{
|
||||
Name: instanceName,
|
||||
})
|
||||
|
||||
if err := miner.Start(config); err != nil {
|
||||
// manager.emitEvent(EventMinerError, MinerEventData{Name: "xmrig-rx_0", Error: err.Error()})
|
||||
// manager.emitEvent(EventMinerError, MinerEventData{Name: "xmrig-rx_0", Error: err.Error()}) reports the failure before returning it.
|
||||
manager.emitEvent(EventMinerError, MinerEventData{
|
||||
Name: instanceName,
|
||||
Error: err.Error(),
|
||||
|
|
@ -346,7 +348,7 @@ func (manager *Manager) StartMiner(ctx context.Context, minerType string, config
|
|||
logMessage := "CryptoCurrency Miner started: " + miner.GetName() + " (Binary: " + miner.GetBinaryPath() + ")"
|
||||
logToSyslog(logMessage)
|
||||
|
||||
// Emit started event
|
||||
// manager.emitEvent(EventMinerStarted, MinerEventData{Name: "xmrig-rx_0"}) marks the miner as running for websocket clients.
|
||||
manager.emitEvent(EventMinerStarted, MinerEventData{
|
||||
Name: instanceName,
|
||||
})
|
||||
|
|
@ -355,10 +357,10 @@ func (manager *Manager) StartMiner(ctx context.Context, minerType string, config
|
|||
return miner, nil
|
||||
}
|
||||
|
||||
// manager.UninstallMiner(ctx, "xmrig") // stops all xmrig instances and removes config
|
||||
// manager.UninstallMiner(ctx, "ttminer") // stops all ttminer instances and removes config
|
||||
// manager.UninstallMiner(ctx, "xmrig") stops all xmrig instances and removes the matching config entry.
|
||||
// manager.UninstallMiner(ctx, "ttminer") stops all ttminer instances and removes the matching config entry.
|
||||
func (manager *Manager) UninstallMiner(ctx context.Context, minerType string) error {
|
||||
// Check for cancellation before acquiring lock
|
||||
// ctx, cancel := context.WithCancel(context.Background()); cancel(); manager.UninstallMiner(ctx, "xmrig") returns context.Canceled before locking.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
|
|
@ -366,7 +368,7 @@ func (manager *Manager) UninstallMiner(ctx context.Context, minerType string) er
|
|||
}
|
||||
|
||||
manager.mutex.Lock()
|
||||
// Collect miners to stop and delete (can't modify map during iteration)
|
||||
// manager.UninstallMiner(ctx, "xmrig") collects every running xmrig instance before removing it from the map.
|
||||
minersToDelete := make([]string, 0)
|
||||
minersToStop := make([]Miner, 0)
|
||||
for name, runningMiner := range manager.miners {
|
||||
|
|
@ -379,13 +381,13 @@ func (manager *Manager) UninstallMiner(ctx context.Context, minerType string) er
|
|||
minersToDelete = append(minersToDelete, name)
|
||||
}
|
||||
}
|
||||
// Delete from map first, then release lock before stopping (Stop may block)
|
||||
// delete(manager.miners, "xmrig-rx_0") happens before stopping miners so Stop can block without holding the lock.
|
||||
for _, name := range minersToDelete {
|
||||
delete(manager.miners, name)
|
||||
}
|
||||
manager.mutex.Unlock()
|
||||
|
||||
// Stop miners outside the lock to avoid blocking
|
||||
// miner.Stop() runs outside the lock so one slow uninstall does not block other manager calls.
|
||||
for i, miner := range minersToStop {
|
||||
if err := miner.Stop(); err != nil {
|
||||
logging.Warn("failed to stop running miner during uninstall", logging.Fields{"miner": minersToDelete[i], "error": err})
|
||||
|
|
@ -416,17 +418,17 @@ func (manager *Manager) UninstallMiner(ctx context.Context, minerType string) er
|
|||
// manager.updateMinerConfig("xmrig", true, config) // saves Autostart=true and the last-used config back to miners.json
|
||||
func (manager *Manager) updateMinerConfig(minerType string, autostart bool, config *Config) error {
|
||||
return UpdateMinersConfig(func(minersConfig *MinersConfig) error {
|
||||
found := false
|
||||
minerFound := false
|
||||
for i, minerConfig := range minersConfig.Miners {
|
||||
if equalFold(minerConfig.MinerType, minerType) {
|
||||
minersConfig.Miners[i].Autostart = autostart
|
||||
minersConfig.Miners[i].Config = config
|
||||
found = true
|
||||
minerFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
if !minerFound {
|
||||
minersConfig.Miners = append(minersConfig.Miners, MinerAutostartConfig{
|
||||
MinerType: minerType,
|
||||
Autostart: autostart,
|
||||
|
|
@ -437,10 +439,10 @@ func (manager *Manager) updateMinerConfig(minerType string, autostart bool, conf
|
|||
})
|
||||
}
|
||||
|
||||
// manager.StopMiner(ctx, "xmrig/monero")
|
||||
// manager.StopMiner(ctx, "ttminer/rtx4090") // still removes if already stopped
|
||||
// manager.StopMiner(ctx, "xmrig/monero") stops the matching miner instance and removes it from the manager map.
|
||||
// manager.StopMiner(ctx, "ttminer/rtx4090") still removes the entry when the miner has already stopped.
|
||||
func (manager *Manager) StopMiner(ctx context.Context, name string) error {
|
||||
// Check for cancellation before acquiring lock
|
||||
// ctx, cancel := context.WithCancel(context.Background()); cancel(); manager.StopMiner(ctx, "xmrig-rx_0") returns context.Canceled before locking.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
|
|
@ -466,19 +468,18 @@ func (manager *Manager) StopMiner(ctx context.Context, name string) error {
|
|||
return ErrMinerNotFound(name)
|
||||
}
|
||||
|
||||
// Emit stopping event
|
||||
// manager.emitEvent(EventMinerStopping, MinerEventData{Name: "xmrig-rx_0"}) tells websocket clients shutdown has started.
|
||||
manager.emitEvent(EventMinerStopping, MinerEventData{
|
||||
Name: name,
|
||||
})
|
||||
|
||||
// Try to stop the miner, but always remove it from the map
|
||||
// This handles the case where a miner crashed or was killed externally
|
||||
// stopErr := miner.Stop() may fail after an external kill, but cleanup continues so the manager state stays accurate.
|
||||
stopErr := miner.Stop()
|
||||
|
||||
// Always remove from map - if it's not running, we still want to clean it up
|
||||
// delete(manager.miners, "xmrig-rx_0") removes stale entries even when the process has already exited.
|
||||
delete(manager.miners, name)
|
||||
|
||||
// Emit stopped event
|
||||
// manager.emitEvent(EventMinerStopped, MinerEventData{Name: "xmrig-rx_0", Reason: "stopped"}) confirms the final stop reason.
|
||||
reason := "stopped"
|
||||
if stopErr != nil && stopErr.Error() != "miner is not running" {
|
||||
reason = stopErr.Error()
|
||||
|
|
@ -488,7 +489,7 @@ func (manager *Manager) StopMiner(ctx context.Context, name string) error {
|
|||
Reason: reason,
|
||||
})
|
||||
|
||||
// Only return error if it wasn't just "miner is not running"
|
||||
// stopErr = errors.New("permission denied") still returns the stop failure after the manager removes the stale entry.
|
||||
if stopErr != nil && stopErr.Error() != "miner is not running" {
|
||||
return stopErr
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,23 +198,23 @@ func generateRequestID() string {
|
|||
return strconv.FormatInt(time.Now().UnixMilli(), 10) + "-" + hex.EncodeToString(randomBytes)
|
||||
}
|
||||
|
||||
// requestID := getRequestID(c) returns "trace-123" after requestIDMiddleware stores the incoming header.
|
||||
func getRequestID(c *gin.Context) string {
|
||||
if id, exists := c.Get("requestID"); exists {
|
||||
if stringValue, ok := id.(string); ok {
|
||||
// requestID := requestIDFromContext(c) returns "trace-123" after requestIDMiddleware stores the header.
|
||||
func requestIDFromContext(c *gin.Context) string {
|
||||
if requestIDValue, exists := c.Get("requestID"); exists {
|
||||
if stringValue, ok := requestIDValue.(string); ok {
|
||||
return stringValue
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// logWithRequestID(c, "error", "miner failed to start", logging.Fields{"type": "xmrig", "name": "xmrig-main"})
|
||||
// logWithRequestID(c, "info", "miner started", logging.Fields{"name": "xmrig-1", "request_id": "trace-123"})
|
||||
func logWithRequestID(c *gin.Context, level string, message string, fields logging.Fields) {
|
||||
// logWithRequestContext(c, "error", "miner failed to start", logging.Fields{"type": "xmrig", "name": "xmrig-main"})
|
||||
// logWithRequestContext(c, "info", "miner started", logging.Fields{"name": "xmrig-1", "request_id": "trace-123"})
|
||||
func logWithRequestContext(c *gin.Context, level string, message string, fields logging.Fields) {
|
||||
if fields == nil {
|
||||
fields = logging.Fields{}
|
||||
}
|
||||
if requestID := getRequestID(c); requestID != "" {
|
||||
if requestID := requestIDFromContext(c); requestID != "" {
|
||||
fields["request_id"] = requestID
|
||||
}
|
||||
switch level {
|
||||
|
|
@ -361,12 +361,12 @@ var wsUpgrader = websocket.Upgrader{
|
|||
if origin == "" {
|
||||
return true // No Origin header, for example from curl or another non-browser client.
|
||||
}
|
||||
// Parse the origin URL properly to prevent bypass attacks.
|
||||
u, err := url.Parse(origin)
|
||||
// parsedOrigin, err := url.Parse("http://localhost:4200") keeps browser-origin checks exact.
|
||||
parsedOrigin, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
host := u.Hostname()
|
||||
host := parsedOrigin.Hostname()
|
||||
// Allow exact localhost matches like `http://127.0.0.1:4200`.
|
||||
return host == "localhost" || host == "127.0.0.1" || host == "::1" ||
|
||||
host == "wails.localhost"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue