refactor: Remove unused code and fix nil dereference issues

- Remove unused exported functions from pkg/database (session tracking,
  bulk hashrate inserts, various query helpers)
- Remove unused exported functions from pkg/node (identity management,
  bundle operations, controller methods)
- Make internal-only functions unexported in config_manager.go and database.go
- Remove unused EventProfile* constants from events.go
- Add GetCommit() and GetBuildDate() to expose version.go variables
- Fix potential nil dereference issues flagged by Qodana:
  - Add nil checks for GetIdentity() in controller.go, transport.go, worker.go
  - Add nil checks for GetPeer() in peer_test.go
  - Add nil checks in worker_test.go

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
snider 2025-12-31 09:24:12 +00:00
parent b0297471c6
commit d61a8aff8b
14 changed files with 148 additions and 636 deletions

View file

@ -28,8 +28,8 @@ type Config struct {
RetentionDays int `json:"retentionDays,omitempty"` RetentionDays int `json:"retentionDays,omitempty"`
} }
// DefaultConfig returns the default database configuration // defaultConfig returns the default database configuration
func DefaultConfig() Config { func defaultConfig() Config {
return Config{ return Config{
Enabled: true, Enabled: true,
Path: "", Path: "",
@ -99,8 +99,8 @@ func Close() error {
return err return err
} }
// IsInitialized returns true if the database is ready // isInitialized returns true if the database is ready
func IsInitialized() bool { func isInitialized() bool {
dbMu.RLock() dbMu.RLock()
defer dbMu.RUnlock() defer dbMu.RUnlock()
return db != nil return db != nil
@ -168,8 +168,8 @@ func Cleanup(retentionDays int) error {
return err return err
} }
// VacuumDB optimizes the database file size // vacuumDB optimizes the database file size
func VacuumDB() error { func vacuumDB() error {
dbMu.RLock() dbMu.RLock()
defer dbMu.RUnlock() defer dbMu.RUnlock()

View file

@ -1,16 +1,11 @@
package database package database
import ( import (
"context"
"database/sql"
"fmt" "fmt"
"log" "log"
"time" "time"
) )
// dbOperationTimeout is the maximum time for database operations
const dbOperationTimeout = 30 * time.Second
// parseSQLiteTimestamp parses timestamp strings from SQLite which may use various formats. // parseSQLiteTimestamp parses timestamp strings from SQLite which may use various formats.
// Logs a warning if parsing fails and returns zero time. // Logs a warning if parsing fails and returns zero time.
func parseSQLiteTimestamp(s string) time.Time { func parseSQLiteTimestamp(s string) time.Time {
@ -68,48 +63,6 @@ func InsertHashratePoint(minerName, minerType string, point HashratePoint, resol
return err return err
} }
// InsertHashratePoints stores multiple hashrate measurements in a single transaction
func InsertHashratePoints(minerName, minerType string, points []HashratePoint, resolution Resolution) error {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return nil
}
if len(points) == 0 {
return nil
}
// Use context with timeout to prevent hanging on locked database
ctx, cancel := context.WithTimeout(context.Background(), dbOperationTimeout)
defer cancel()
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback()
stmt, err := tx.PrepareContext(ctx, `
INSERT INTO hashrate_history (miner_name, miner_type, timestamp, hashrate, resolution)
VALUES (?, ?, ?, ?, ?)
`)
if err != nil {
return fmt.Errorf("failed to prepare statement: %w", err)
}
defer stmt.Close()
for _, point := range points {
_, err := stmt.ExecContext(ctx, minerName, minerType, point.Timestamp, point.Hashrate, string(resolution))
if err != nil {
return fmt.Errorf("failed to insert point: %w", err)
}
}
return tx.Commit()
}
// GetHashrateHistory retrieves hashrate history for a miner within a time range // GetHashrateHistory retrieves hashrate history for a miner within a time range
func GetHashrateHistory(minerName string, resolution Resolution, since, until time.Time) ([]HashratePoint, error) { func GetHashrateHistory(minerName string, resolution Resolution, since, until time.Time) ([]HashratePoint, error) {
dbMu.RLock() dbMu.RLock()
@ -145,76 +98,6 @@ func GetHashrateHistory(minerName string, resolution Resolution, since, until ti
return points, rows.Err() return points, rows.Err()
} }
// GetLatestHashrate retrieves the most recent hashrate for a miner
func GetLatestHashrate(minerName string) (*HashratePoint, error) {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return nil, nil
}
var point HashratePoint
err := db.QueryRow(`
SELECT timestamp, hashrate
FROM hashrate_history
WHERE miner_name = ?
ORDER BY timestamp DESC
LIMIT 1
`, minerName).Scan(&point.Timestamp, &point.Hashrate)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil // No data found is not an error
}
return nil, fmt.Errorf("failed to get latest hashrate: %w", err)
}
return &point, nil
}
// GetAverageHashrate calculates the average hashrate for a miner in a time range
func GetAverageHashrate(minerName string, since, until time.Time) (int, error) {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return 0, nil
}
var avg float64
err := db.QueryRow(`
SELECT COALESCE(AVG(hashrate), 0)
FROM hashrate_history
WHERE miner_name = ?
AND timestamp >= ?
AND timestamp <= ?
`, minerName, since, until).Scan(&avg)
return int(avg), err
}
// GetMaxHashrate retrieves the maximum hashrate for a miner in a time range
func GetMaxHashrate(minerName string, since, until time.Time) (int, error) {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return 0, nil
}
var max int
err := db.QueryRow(`
SELECT COALESCE(MAX(hashrate), 0)
FROM hashrate_history
WHERE miner_name = ?
AND timestamp >= ?
AND timestamp <= ?
`, minerName, since, until).Scan(&max)
return max, err
}
// GetHashrateStats retrieves aggregated stats for a miner // GetHashrateStats retrieves aggregated stats for a miner
type HashrateStats struct { type HashrateStats struct {
MinerName string `json:"minerName"` MinerName string `json:"minerName"`
@ -331,22 +214,3 @@ func GetAllMinerStats() ([]HashrateStats, error) {
return allStats, rows.Err() return allStats, rows.Err()
} }
// CleanupOldData removes hashrate data older than the specified duration
func CleanupOldData(resolution Resolution, maxAge time.Duration) error {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return nil
}
cutoff := time.Now().Add(-maxAge)
_, err := db.Exec(`
DELETE FROM hashrate_history
WHERE resolution = ?
AND timestamp < ?
`, string(resolution), cutoff)
return err
}

View file

@ -1,279 +1,5 @@
package database package database
import ( // This file previously contained session tracking functions.
"fmt" // Session tracking is not currently integrated into the mining manager.
"time" // The database schema still supports sessions for future use.
)
// MinerSession represents a mining session
type MinerSession struct {
ID int64 `json:"id"`
MinerName string `json:"minerName"`
MinerType string `json:"minerType"`
StartedAt time.Time `json:"startedAt"`
StoppedAt *time.Time `json:"stoppedAt,omitempty"`
TotalShares int `json:"totalShares"`
RejectedShares int `json:"rejectedShares"`
AverageHashrate int `json:"averageHashrate"`
}
// StartSession records the start of a new mining session
func StartSession(minerName, minerType string) (int64, error) {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return 0, nil
}
result, err := db.Exec(`
INSERT INTO miner_sessions (miner_name, miner_type, started_at)
VALUES (?, ?, ?)
`, minerName, minerType, time.Now())
if err != nil {
return 0, fmt.Errorf("failed to start session: %w", err)
}
return result.LastInsertId()
}
// EndSession marks a session as complete with final stats
func EndSession(sessionID int64, totalShares, rejectedShares, averageHashrate int) error {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return nil
}
_, err := db.Exec(`
UPDATE miner_sessions
SET stopped_at = ?,
total_shares = ?,
rejected_shares = ?,
average_hashrate = ?
WHERE id = ?
`, time.Now(), totalShares, rejectedShares, averageHashrate, sessionID)
return err
}
// EndSessionByName marks the most recent session for a miner as complete
func EndSessionByName(minerName string, totalShares, rejectedShares, averageHashrate int) error {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return nil
}
_, err := db.Exec(`
UPDATE miner_sessions
SET stopped_at = ?,
total_shares = ?,
rejected_shares = ?,
average_hashrate = ?
WHERE miner_name = ?
AND stopped_at IS NULL
`, time.Now(), totalShares, rejectedShares, averageHashrate, minerName)
return err
}
// GetSession retrieves a session by ID
func GetSession(sessionID int64) (*MinerSession, error) {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return nil, nil
}
var session MinerSession
var stoppedAt *time.Time
err := db.QueryRow(`
SELECT id, miner_name, miner_type, started_at, stopped_at,
total_shares, rejected_shares, average_hashrate
FROM miner_sessions
WHERE id = ?
`, sessionID).Scan(
&session.ID,
&session.MinerName,
&session.MinerType,
&session.StartedAt,
&stoppedAt,
&session.TotalShares,
&session.RejectedShares,
&session.AverageHashrate,
)
if err != nil {
return nil, err
}
session.StoppedAt = stoppedAt
return &session, nil
}
// GetActiveSessions retrieves all currently active (non-stopped) sessions
func GetActiveSessions() ([]MinerSession, error) {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return nil, nil
}
rows, err := db.Query(`
SELECT id, miner_name, miner_type, started_at, stopped_at,
total_shares, rejected_shares, average_hashrate
FROM miner_sessions
WHERE stopped_at IS NULL
ORDER BY started_at DESC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var sessions []MinerSession
for rows.Next() {
var session MinerSession
var stoppedAt *time.Time
if err := rows.Scan(
&session.ID,
&session.MinerName,
&session.MinerType,
&session.StartedAt,
&stoppedAt,
&session.TotalShares,
&session.RejectedShares,
&session.AverageHashrate,
); err != nil {
return nil, err
}
session.StoppedAt = stoppedAt
sessions = append(sessions, session)
}
return sessions, rows.Err()
}
// GetRecentSessions retrieves the most recent sessions for a miner
func GetRecentSessions(minerName string, limit int) ([]MinerSession, error) {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return nil, nil
}
rows, err := db.Query(`
SELECT id, miner_name, miner_type, started_at, stopped_at,
total_shares, rejected_shares, average_hashrate
FROM miner_sessions
WHERE miner_name = ?
ORDER BY started_at DESC
LIMIT ?
`, minerName, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var sessions []MinerSession
for rows.Next() {
var session MinerSession
var stoppedAt *time.Time
if err := rows.Scan(
&session.ID,
&session.MinerName,
&session.MinerType,
&session.StartedAt,
&stoppedAt,
&session.TotalShares,
&session.RejectedShares,
&session.AverageHashrate,
); err != nil {
return nil, err
}
session.StoppedAt = stoppedAt
sessions = append(sessions, session)
}
return sessions, rows.Err()
}
// GetSessionStats retrieves aggregated session statistics for a miner
type SessionStats struct {
MinerName string `json:"minerName"`
TotalSessions int `json:"totalSessions"`
TotalUptime time.Duration `json:"totalUptime"`
TotalShares int `json:"totalShares"`
TotalRejected int `json:"totalRejected"`
AvgSessionTime time.Duration `json:"avgSessionTime"`
AvgHashrate int `json:"avgHashrate"`
LastSessionAt time.Time `json:"lastSessionAt"`
}
func GetSessionStats(minerName string) (*SessionStats, error) {
dbMu.RLock()
defer dbMu.RUnlock()
if db == nil {
return nil, nil
}
var stats SessionStats
stats.MinerName = minerName
// Get basic aggregates
err := db.QueryRow(`
SELECT
COUNT(*),
COALESCE(SUM(total_shares), 0),
COALESCE(SUM(rejected_shares), 0),
COALESCE(AVG(average_hashrate), 0),
MAX(started_at)
FROM miner_sessions
WHERE miner_name = ?
AND stopped_at IS NOT NULL
`, minerName).Scan(
&stats.TotalSessions,
&stats.TotalShares,
&stats.TotalRejected,
&stats.AvgHashrate,
&stats.LastSessionAt,
)
if err != nil {
return nil, err
}
// Calculate total uptime
rows, err := db.Query(`
SELECT started_at, stopped_at
FROM miner_sessions
WHERE miner_name = ?
AND stopped_at IS NOT NULL
`, minerName)
if err != nil {
return nil, err
}
defer rows.Close()
var totalSeconds int64
for rows.Next() {
var started, stopped time.Time
if err := rows.Scan(&started, &stopped); err != nil {
return nil, err
}
totalSeconds += int64(stopped.Sub(started).Seconds())
}
stats.TotalUptime = time.Duration(totalSeconds) * time.Second
if stats.TotalSessions > 0 {
stats.AvgSessionTime = stats.TotalUptime / time.Duration(stats.TotalSessions)
}
return &stats, nil
}

View file

@ -28,8 +28,8 @@ type DatabaseConfig struct {
RetentionDays int `json:"retentionDays,omitempty"` RetentionDays int `json:"retentionDays,omitempty"`
} }
// DefaultDatabaseConfig returns the default database configuration. // defaultDatabaseConfig returns the default database configuration.
func DefaultDatabaseConfig() DatabaseConfig { func defaultDatabaseConfig() DatabaseConfig {
return DatabaseConfig{ return DatabaseConfig{
Enabled: true, Enabled: true,
RetentionDays: 30, RetentionDays: 30,
@ -42,8 +42,8 @@ type MinersConfig struct {
Database DatabaseConfig `json:"database"` Database DatabaseConfig `json:"database"`
} }
// GetMinersConfigPath returns the path to the miners configuration file. // getMinersConfigPath returns the path to the miners configuration file.
func GetMinersConfigPath() (string, error) { func getMinersConfigPath() (string, error) {
return xdg.ConfigFile("lethean-desktop/miners/config.json") return xdg.ConfigFile("lethean-desktop/miners/config.json")
} }
@ -52,7 +52,7 @@ func LoadMinersConfig() (*MinersConfig, error) {
configMu.RLock() configMu.RLock()
defer configMu.RUnlock() defer configMu.RUnlock()
configPath, err := GetMinersConfigPath() configPath, err := getMinersConfigPath()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not determine miners config path: %w", err) return nil, fmt.Errorf("could not determine miners config path: %w", err)
} }
@ -63,7 +63,7 @@ func LoadMinersConfig() (*MinersConfig, error) {
// Return empty config with defaults if file doesn't exist // Return empty config with defaults if file doesn't exist
return &MinersConfig{ return &MinersConfig{
Miners: []MinerAutostartConfig{}, Miners: []MinerAutostartConfig{},
Database: DefaultDatabaseConfig(), Database: defaultDatabaseConfig(),
}, nil }, nil
} }
return nil, fmt.Errorf("failed to read miners config file: %w", err) return nil, fmt.Errorf("failed to read miners config file: %w", err)
@ -76,7 +76,7 @@ func LoadMinersConfig() (*MinersConfig, error) {
// Apply default database config if not set (for backwards compatibility) // Apply default database config if not set (for backwards compatibility)
if cfg.Database.RetentionDays == 0 { if cfg.Database.RetentionDays == 0 {
cfg.Database = DefaultDatabaseConfig() cfg.Database = defaultDatabaseConfig()
} }
return &cfg, nil return &cfg, nil
@ -88,7 +88,7 @@ func SaveMinersConfig(cfg *MinersConfig) error {
configMu.Lock() configMu.Lock()
defer configMu.Unlock() defer configMu.Unlock()
configPath, err := GetMinersConfigPath() configPath, err := getMinersConfigPath()
if err != nil { if err != nil {
return fmt.Errorf("could not determine miners config path: %w", err) return fmt.Errorf("could not determine miners config path: %w", err)
} }

View file

@ -22,11 +22,6 @@ const (
EventMinerError EventType = "miner.error" EventMinerError EventType = "miner.error"
EventMinerConnected EventType = "miner.connected" EventMinerConnected EventType = "miner.connected"
// Profile events
EventProfileCreated EventType = "profile.created"
EventProfileUpdated EventType = "profile.updated"
EventProfileDeleted EventType = "profile.deleted"
// System events // System events
EventPong EventType = "pong" EventPong EventType = "pong"
) )

View file

@ -10,3 +10,13 @@ var (
func GetVersion() string { func GetVersion() string {
return version return version
} }
// GetCommit returns the git commit hash
func GetCommit() string {
return commit
}
// GetBuildDate returns the build date
func GetBuildDate() string {
return date
}

View file

@ -129,71 +129,6 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo
}, nil }, nil
} }
// CreateFullBundle creates an encrypted bundle with miners and all profiles.
func CreateFullBundle(minerPaths []string, profiles [][]byte, name string, password string) (*Bundle, error) {
files := make(map[string][]byte)
// Add each miner
for _, minerPath := range minerPaths {
minerData, err := os.ReadFile(minerPath)
if err != nil {
return nil, fmt.Errorf("failed to read miner %s: %w", minerPath, err)
}
files["miners/"+filepath.Base(minerPath)] = minerData
}
// Add each profile
for i, profile := range profiles {
profileName := fmt.Sprintf("profiles/profile_%d.json", i)
files[profileName] = profile
}
// Create tarball
tarData, err := createTarball(files)
if err != nil {
return nil, fmt.Errorf("failed to create tarball: %w", err)
}
// Create DataNode from tarball
dn, err := datanode.FromTar(tarData)
if err != nil {
return nil, fmt.Errorf("failed to create datanode: %w", err)
}
// Create TIM from DataNode
t, err := tim.FromDataNode(dn)
if err != nil {
return nil, fmt.Errorf("failed to create TIM: %w", err)
}
// Create manifest as config
manifest := BundleManifest{
Type: BundleFull,
Name: name,
ProfileIDs: make([]string, len(profiles)),
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
return nil, fmt.Errorf("failed to create manifest: %w", err)
}
t.Config = manifestJSON
// Encrypt to STIM format
stimData, err := t.ToSigil(password)
if err != nil {
return nil, fmt.Errorf("failed to encrypt bundle: %w", err)
}
checksum := calculateChecksum(stimData)
return &Bundle{
Type: BundleFull,
Name: name,
Data: stimData,
Checksum: checksum,
}, nil
}
// ExtractProfileBundle decrypts and extracts a profile bundle. // ExtractProfileBundle decrypts and extracts a profile bundle.
func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) { func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) {
// Verify checksum first // Verify checksum first
@ -243,39 +178,6 @@ func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string
return minerPath, t.Config, nil return minerPath, t.Config, nil
} }
// ExtractFullBundle decrypts and extracts a full bundle.
func ExtractFullBundle(bundle *Bundle, password string, destDir string) (*BundleManifest, error) {
// Verify checksum
if calculateChecksum(bundle.Data) != bundle.Checksum {
return nil, fmt.Errorf("checksum mismatch - bundle may be corrupted")
}
// Decrypt STIM format
t, err := tim.FromSigil(bundle.Data, password)
if err != nil {
return nil, fmt.Errorf("failed to decrypt bundle: %w", err)
}
// Parse manifest
var manifest BundleManifest
if err := json.Unmarshal(t.Config, &manifest); err != nil {
return nil, fmt.Errorf("failed to parse manifest: %w", err)
}
// Convert rootfs to tarball and extract
tarData, err := t.RootFS.ToTar()
if err != nil {
return nil, fmt.Errorf("failed to convert rootfs to tar: %w", err)
}
// Extract tarball to destination
if _, err := extractTarball(tarData, destDir); err != nil {
return nil, fmt.Errorf("failed to extract tarball: %w", err)
}
return &manifest, nil
}
// VerifyBundle checks if a bundle's checksum is valid. // VerifyBundle checks if a bundle's checksum is valid.
func VerifyBundle(bundle *Bundle) bool { func VerifyBundle(bundle *Bundle) bool {
return calculateChecksum(bundle.Data) == bundle.Checksum return calculateChecksum(bundle.Data) == bundle.Checksum

View file

@ -111,6 +111,9 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat
// GetRemoteStats requests miner statistics from a remote peer. // GetRemoteStats requests miner statistics from a remote peer.
func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) { func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) {
identity := c.node.GetIdentity() identity := c.node.GetIdentity()
if identity == nil {
return nil, fmt.Errorf("node identity not initialized")
}
msg, err := NewMessage(MsgGetStats, identity.ID, peerID, nil) msg, err := NewMessage(MsgGetStats, identity.ID, peerID, nil)
if err != nil { if err != nil {
@ -145,6 +148,9 @@ func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) {
// StartRemoteMiner requests a remote peer to start a miner with a given profile. // StartRemoteMiner requests a remote peer to start a miner with a given profile.
func (c *Controller) StartRemoteMiner(peerID, profileID string, configOverride json.RawMessage) error { func (c *Controller) StartRemoteMiner(peerID, profileID string, configOverride json.RawMessage) error {
identity := c.node.GetIdentity() identity := c.node.GetIdentity()
if identity == nil {
return fmt.Errorf("node identity not initialized")
}
payload := StartMinerPayload{ payload := StartMinerPayload{
ProfileID: profileID, ProfileID: profileID,
@ -188,6 +194,9 @@ func (c *Controller) StartRemoteMiner(peerID, profileID string, configOverride j
// StopRemoteMiner requests a remote peer to stop a miner. // StopRemoteMiner requests a remote peer to stop a miner.
func (c *Controller) StopRemoteMiner(peerID, minerName string) error { func (c *Controller) StopRemoteMiner(peerID, minerName string) error {
identity := c.node.GetIdentity() identity := c.node.GetIdentity()
if identity == nil {
return fmt.Errorf("node identity not initialized")
}
payload := StopMinerPayload{ payload := StopMinerPayload{
MinerName: minerName, MinerName: minerName,
@ -230,6 +239,9 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error {
// GetRemoteLogs requests console logs from a remote miner. // GetRemoteLogs requests console logs from a remote miner.
func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]string, error) { func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]string, error) {
identity := c.node.GetIdentity() identity := c.node.GetIdentity()
if identity == nil {
return nil, fmt.Errorf("node identity not initialized")
}
payload := GetLogsPayload{ payload := GetLogsPayload{
MinerName: minerName, MinerName: minerName,
@ -266,51 +278,6 @@ func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]strin
return logs.Lines, nil return logs.Lines, nil
} }
// DeployProfile sends a profile configuration to a remote peer.
func (c *Controller) DeployProfile(peerID string, bundleData []byte, name string, checksum string) error {
identity := c.node.GetIdentity()
payload := DeployPayload{
BundleType: "profile",
Data: bundleData,
Checksum: checksum,
Name: name,
}
msg, err := NewMessage(MsgDeploy, identity.ID, peerID, payload)
if err != nil {
return fmt.Errorf("failed to create message: %w", err)
}
resp, err := c.sendRequest(peerID, msg, 60*time.Second)
if err != nil {
return err
}
if resp.Type == MsgError {
var errPayload ErrorPayload
if err := resp.ParsePayload(&errPayload); err != nil {
return fmt.Errorf("remote error (unable to parse)")
}
return fmt.Errorf("remote error: %s", errPayload.Message)
}
if resp.Type != MsgDeployAck {
return fmt.Errorf("unexpected response type: %s", resp.Type)
}
var ack DeployAckPayload
if err := resp.ParsePayload(&ack); err != nil {
return fmt.Errorf("failed to parse ack: %w", err)
}
if !ack.Success {
return fmt.Errorf("deployment failed: %s", ack.Error)
}
return nil
}
// GetAllStats fetches stats from all connected peers. // GetAllStats fetches stats from all connected peers.
func (c *Controller) GetAllStats() map[string]*StatsPayload { func (c *Controller) GetAllStats() map[string]*StatsPayload {
peers := c.peers.GetConnectedPeers() peers := c.peers.GetConnectedPeers()
@ -336,26 +303,12 @@ func (c *Controller) GetAllStats() map[string]*StatsPayload {
return results return results
} }
// GetTotalHashrate calculates total hashrate across all connected peers.
func (c *Controller) GetTotalHashrate() float64 {
allStats := c.GetAllStats()
var total float64
for _, stats := range allStats {
if stats == nil {
continue
}
for _, miner := range stats.Miners {
total += miner.Hashrate
}
}
return total
}
// PingPeer sends a ping to a peer and updates metrics. // PingPeer sends a ping to a peer and updates metrics.
func (c *Controller) PingPeer(peerID string) (float64, error) { func (c *Controller) PingPeer(peerID string) (float64, error) {
identity := c.node.GetIdentity() identity := c.node.GetIdentity()
if identity == nil {
return 0, fmt.Errorf("node identity not initialized")
}
sentAt := time.Now() sentAt := time.Now()
payload := PingPayload{ payload := PingPayload{

View file

@ -171,16 +171,6 @@ func (n *NodeManager) DeriveSharedSecret(peerPubKeyBase64 string) ([]byte, error
return hash[:], nil return hash[:], nil
} }
// GetPublicKey returns the node's public key in base64 format.
func (n *NodeManager) GetPublicKey() string {
n.mu.RLock()
defer n.mu.RUnlock()
if n.identity == nil {
return ""
}
return n.identity.PublicKey
}
// savePrivateKey saves the private key to disk with restricted permissions. // savePrivateKey saves the private key to disk with restricted permissions.
func (n *NodeManager) savePrivateKey() error { func (n *NodeManager) savePrivateKey() error {
// Ensure directory exists // Ensure directory exists
@ -249,32 +239,6 @@ func (n *NodeManager) loadIdentity() error {
return nil return nil
} }
// UpdateName updates the node's display name.
func (n *NodeManager) UpdateName(name string) error {
n.mu.Lock()
defer n.mu.Unlock()
if n.identity == nil {
return fmt.Errorf("node identity not initialized")
}
n.identity.Name = name
return n.saveIdentity()
}
// UpdateRole updates the node's operational role.
func (n *NodeManager) UpdateRole(role NodeRole) error {
n.mu.Lock()
defer n.mu.Unlock()
if n.identity == nil {
return fmt.Errorf("node identity not initialized")
}
n.identity.Role = role
return n.saveIdentity()
}
// Delete removes the node identity and keys from disk. // Delete removes the node identity and keys from disk.
func (n *NodeManager) Delete() error { func (n *NodeManager) Delete() error {
n.mu.Lock() n.mu.Lock()

View file

@ -168,6 +168,9 @@ func TestPeerRegistry_UpdateMetrics(t *testing.T) {
} }
updated := pr.GetPeer("metrics-test") updated := pr.GetPeer("metrics-test")
if updated == nil {
t.Fatal("expected peer to exist")
}
if updated.PingMS != 50.5 { if updated.PingMS != 50.5 {
t.Errorf("expected ping 50.5, got %f", updated.PingMS) t.Errorf("expected ping 50.5, got %f", updated.PingMS)
} }
@ -197,6 +200,9 @@ func TestPeerRegistry_UpdateScore(t *testing.T) {
} }
updated := pr.GetPeer("score-test") updated := pr.GetPeer("score-test")
if updated == nil {
t.Fatal("expected peer to exist")
}
if updated.Score != 85.5 { if updated.Score != 85.5 {
t.Errorf("expected score 85.5, got %f", updated.Score) t.Errorf("expected score 85.5, got %f", updated.Score)
} }
@ -208,6 +214,9 @@ func TestPeerRegistry_UpdateScore(t *testing.T) {
} }
updated = pr.GetPeer("score-test") updated = pr.GetPeer("score-test")
if updated == nil {
t.Fatal("expected peer to exist")
}
if updated.Score != 100 { if updated.Score != 100 {
t.Errorf("expected score clamped to 100, got %f", updated.Score) t.Errorf("expected score clamped to 100, got %f", updated.Score)
} }
@ -219,6 +228,9 @@ func TestPeerRegistry_UpdateScore(t *testing.T) {
} }
updated = pr.GetPeer("score-test") updated = pr.GetPeer("score-test")
if updated == nil {
t.Fatal("expected peer to exist")
}
if updated.Score != 0 { if updated.Score != 0 {
t.Errorf("expected score clamped to 0, got %f", updated.Score) t.Errorf("expected score clamped to 0, got %f", updated.Score)
} }
@ -239,6 +251,9 @@ func TestPeerRegistry_SetConnected(t *testing.T) {
pr.SetConnected("connect-test", true) pr.SetConnected("connect-test", true)
updated := pr.GetPeer("connect-test") updated := pr.GetPeer("connect-test")
if updated == nil {
t.Fatal("expected peer to exist")
}
if !updated.Connected { if !updated.Connected {
t.Error("peer should be connected") t.Error("peer should be connected")
} }
@ -248,6 +263,9 @@ func TestPeerRegistry_SetConnected(t *testing.T) {
pr.SetConnected("connect-test", false) pr.SetConnected("connect-test", false)
updated = pr.GetPeer("connect-test") updated = pr.GetPeer("connect-test")
if updated == nil {
t.Fatal("expected peer to exist")
}
if updated.Connected { if updated.Connected {
t.Error("peer should be disconnected") t.Error("peer should be disconnected")
} }

View file

@ -327,6 +327,10 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) {
// Send handshake acknowledgment // Send handshake acknowledgment
identity := t.node.GetIdentity() identity := t.node.GetIdentity()
if identity == nil {
conn.Close()
return
}
ackPayload := HandshakeAckPayload{ ackPayload := HandshakeAckPayload{
Identity: *identity, Identity: *identity,
Accepted: true, Accepted: true,
@ -380,6 +384,9 @@ func (t *Transport) performHandshake(pc *PeerConnection) error {
}() }()
identity := t.node.GetIdentity() identity := t.node.GetIdentity()
if identity == nil {
return fmt.Errorf("node identity not initialized")
}
payload := HandshakePayload{ payload := HandshakePayload{
Identity: *identity, Identity: *identity,

View file

@ -83,14 +83,17 @@ func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message) {
if err != nil { if err != nil {
// Send error response // Send error response
errMsg, _ := NewErrorMessage( identity := w.node.GetIdentity()
w.node.GetIdentity().ID, if identity != nil {
msg.From, errMsg, _ := NewErrorMessage(
ErrCodeOperationFailed, identity.ID,
err.Error(), msg.From,
msg.ID, ErrCodeOperationFailed,
) err.Error(),
conn.Send(errMsg) msg.ID,
)
conn.Send(errMsg)
}
return return
} }
@ -122,6 +125,9 @@ func (w *Worker) handlePing(msg *Message) (*Message, error) {
// handleGetStats responds with current miner statistics. // handleGetStats responds with current miner statistics.
func (w *Worker) handleGetStats(msg *Message) (*Message, error) { func (w *Worker) handleGetStats(msg *Message) (*Message, error) {
identity := w.node.GetIdentity() identity := w.node.GetIdentity()
if identity == nil {
return nil, fmt.Errorf("node identity not initialized")
}
stats := StatsPayload{ stats := StatsPayload{
NodeID: identity.ID, NodeID: identity.ID,

View file

@ -127,6 +127,9 @@ func TestWorker_HandlePing(t *testing.T) {
// Create a ping message // Create a ping message
identity := nm.GetIdentity() identity := nm.GetIdentity()
if identity == nil {
t.Fatal("expected identity to be generated")
}
pingPayload := PingPayload{SentAt: time.Now().UnixMilli()} pingPayload := PingPayload{SentAt: time.Now().UnixMilli()}
pingMsg, err := NewMessage(MsgPing, "sender-id", identity.ID, pingPayload) pingMsg, err := NewMessage(MsgPing, "sender-id", identity.ID, pingPayload)
if err != nil { if err != nil {
@ -183,6 +186,9 @@ func TestWorker_HandleGetStats(t *testing.T) {
// Create a get_stats message // Create a get_stats message
identity := nm.GetIdentity() identity := nm.GetIdentity()
if identity == nil {
t.Fatal("expected identity to be generated")
}
msg, err := NewMessage(MsgGetStats, "sender-id", identity.ID, nil) msg, err := NewMessage(MsgGetStats, "sender-id", identity.ID, nil)
if err != nil { if err != nil {
t.Fatalf("failed to create get_stats message: %v", err) t.Fatalf("failed to create get_stats message: %v", err)
@ -238,6 +244,9 @@ func TestWorker_HandleStartMiner_NoManager(t *testing.T) {
// Create a start_miner message // Create a start_miner message
identity := nm.GetIdentity() identity := nm.GetIdentity()
if identity == nil {
t.Fatal("expected identity to be generated")
}
payload := StartMinerPayload{ProfileID: "test-profile"} payload := StartMinerPayload{ProfileID: "test-profile"}
msg, err := NewMessage(MsgStartMiner, "sender-id", identity.ID, payload) msg, err := NewMessage(MsgStartMiner, "sender-id", identity.ID, payload)
if err != nil { if err != nil {
@ -273,6 +282,9 @@ func TestWorker_HandleStopMiner_NoManager(t *testing.T) {
// Create a stop_miner message // Create a stop_miner message
identity := nm.GetIdentity() identity := nm.GetIdentity()
if identity == nil {
t.Fatal("expected identity to be generated")
}
payload := StopMinerPayload{MinerName: "test-miner"} payload := StopMinerPayload{MinerName: "test-miner"}
msg, err := NewMessage(MsgStopMiner, "sender-id", identity.ID, payload) msg, err := NewMessage(MsgStopMiner, "sender-id", identity.ID, payload)
if err != nil { if err != nil {
@ -308,6 +320,9 @@ func TestWorker_HandleGetLogs_NoManager(t *testing.T) {
// Create a get_logs message // Create a get_logs message
identity := nm.GetIdentity() identity := nm.GetIdentity()
if identity == nil {
t.Fatal("expected identity to be generated")
}
payload := GetLogsPayload{MinerName: "test-miner", Lines: 100} payload := GetLogsPayload{MinerName: "test-miner", Lines: 100}
msg, err := NewMessage(MsgGetLogs, "sender-id", identity.ID, payload) msg, err := NewMessage(MsgGetLogs, "sender-id", identity.ID, payload)
if err != nil { if err != nil {
@ -343,6 +358,9 @@ func TestWorker_HandleDeploy_Profile(t *testing.T) {
// Create a deploy message for profile // Create a deploy message for profile
identity := nm.GetIdentity() identity := nm.GetIdentity()
if identity == nil {
t.Fatal("expected identity to be generated")
}
payload := DeployPayload{ payload := DeployPayload{
BundleType: "profile", BundleType: "profile",
Data: []byte(`{"id": "test", "name": "Test Profile"}`), Data: []byte(`{"id": "test", "name": "Test Profile"}`),
@ -382,6 +400,9 @@ func TestWorker_HandleDeploy_UnknownType(t *testing.T) {
// Create a deploy message with unknown type // Create a deploy message with unknown type
identity := nm.GetIdentity() identity := nm.GetIdentity()
if identity == nil {
t.Fatal("expected identity to be generated")
}
payload := DeployPayload{ payload := DeployPayload{
BundleType: "unknown", BundleType: "unknown",
Data: []byte(`{}`), Data: []byte(`{}`),

46
qodana.yaml Normal file
View file

@ -0,0 +1,46 @@
#-------------------------------------------------------------------------------#
# Qodana analysis is configured by qodana.yaml file #
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
#-------------------------------------------------------------------------------#
#################################################################################
# WARNING: Do not store sensitive information in this file, #
# as its contents will be included in the Qodana report. #
#################################################################################
version: "1.0"
#Specify inspection profile for code analysis
profile:
name: qodana.starter
#Enable inspections
#include:
# - name: <SomeEnabledInspectionId>
#Disable inspections
#exclude:
# - name: <SomeDisabledInspectionId>
# paths:
# - <path/where/not/run/inspection>
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
#bootstrap: sh ./prepare-qodana.sh
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
#plugins:
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
# Quality gate. Will fail the CI/CD pipeline if any condition is not met
# severityThresholds - configures maximum thresholds for different problem severities
# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code
# Code Coverage is available in Ultimate and Ultimate Plus plans
#failureConditions:
# severityThresholds:
# any: 15
# critical: 5
# testCoverageThresholds:
# fresh: 70
# total: 50
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
linter: jetbrains/qodana-go:2025.3