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:
parent
b0297471c6
commit
d61a8aff8b
14 changed files with 148 additions and 636 deletions
|
|
@ -28,8 +28,8 @@ type Config struct {
|
|||
RetentionDays int `json:"retentionDays,omitempty"`
|
||||
}
|
||||
|
||||
// DefaultConfig returns the default database configuration
|
||||
func DefaultConfig() Config {
|
||||
// defaultConfig returns the default database configuration
|
||||
func defaultConfig() Config {
|
||||
return Config{
|
||||
Enabled: true,
|
||||
Path: "",
|
||||
|
|
@ -99,8 +99,8 @@ func Close() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// IsInitialized returns true if the database is ready
|
||||
func IsInitialized() bool {
|
||||
// isInitialized returns true if the database is ready
|
||||
func isInitialized() bool {
|
||||
dbMu.RLock()
|
||||
defer dbMu.RUnlock()
|
||||
return db != nil
|
||||
|
|
@ -168,8 +168,8 @@ func Cleanup(retentionDays int) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// VacuumDB optimizes the database file size
|
||||
func VacuumDB() error {
|
||||
// vacuumDB optimizes the database file size
|
||||
func vacuumDB() error {
|
||||
dbMu.RLock()
|
||||
defer dbMu.RUnlock()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,11 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"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.
|
||||
// Logs a warning if parsing fails and returns zero time.
|
||||
func parseSQLiteTimestamp(s string) time.Time {
|
||||
|
|
@ -68,48 +63,6 @@ func InsertHashratePoint(minerName, minerType string, point HashratePoint, resol
|
|||
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
|
||||
func GetHashrateHistory(minerName string, resolution Resolution, since, until time.Time) ([]HashratePoint, error) {
|
||||
dbMu.RLock()
|
||||
|
|
@ -145,76 +98,6 @@ func GetHashrateHistory(minerName string, resolution Resolution, since, until ti
|
|||
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
|
||||
type HashrateStats struct {
|
||||
MinerName string `json:"minerName"`
|
||||
|
|
@ -331,22 +214,3 @@ func GetAllMinerStats() ([]HashrateStats, error) {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,279 +1,5 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
// This file previously contained session tracking functions.
|
||||
// Session tracking is not currently integrated into the mining manager.
|
||||
// The database schema still supports sessions for future use.
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ type DatabaseConfig struct {
|
|||
RetentionDays int `json:"retentionDays,omitempty"`
|
||||
}
|
||||
|
||||
// DefaultDatabaseConfig returns the default database configuration.
|
||||
func DefaultDatabaseConfig() DatabaseConfig {
|
||||
// defaultDatabaseConfig returns the default database configuration.
|
||||
func defaultDatabaseConfig() DatabaseConfig {
|
||||
return DatabaseConfig{
|
||||
Enabled: true,
|
||||
RetentionDays: 30,
|
||||
|
|
@ -42,8 +42,8 @@ type MinersConfig struct {
|
|||
Database DatabaseConfig `json:"database"`
|
||||
}
|
||||
|
||||
// GetMinersConfigPath returns the path to the miners configuration file.
|
||||
func GetMinersConfigPath() (string, error) {
|
||||
// getMinersConfigPath returns the path to the miners configuration file.
|
||||
func getMinersConfigPath() (string, error) {
|
||||
return xdg.ConfigFile("lethean-desktop/miners/config.json")
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ func LoadMinersConfig() (*MinersConfig, error) {
|
|||
configMu.RLock()
|
||||
defer configMu.RUnlock()
|
||||
|
||||
configPath, err := GetMinersConfigPath()
|
||||
configPath, err := getMinersConfigPath()
|
||||
if err != nil {
|
||||
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 &MinersConfig{
|
||||
Miners: []MinerAutostartConfig{},
|
||||
Database: DefaultDatabaseConfig(),
|
||||
Database: defaultDatabaseConfig(),
|
||||
}, nil
|
||||
}
|
||||
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)
|
||||
if cfg.Database.RetentionDays == 0 {
|
||||
cfg.Database = DefaultDatabaseConfig()
|
||||
cfg.Database = defaultDatabaseConfig()
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
|
|
@ -88,7 +88,7 @@ func SaveMinersConfig(cfg *MinersConfig) error {
|
|||
configMu.Lock()
|
||||
defer configMu.Unlock()
|
||||
|
||||
configPath, err := GetMinersConfigPath()
|
||||
configPath, err := getMinersConfigPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not determine miners config path: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,11 +22,6 @@ const (
|
|||
EventMinerError EventType = "miner.error"
|
||||
EventMinerConnected EventType = "miner.connected"
|
||||
|
||||
// Profile events
|
||||
EventProfileCreated EventType = "profile.created"
|
||||
EventProfileUpdated EventType = "profile.updated"
|
||||
EventProfileDeleted EventType = "profile.deleted"
|
||||
|
||||
// System events
|
||||
EventPong EventType = "pong"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,3 +10,13 @@ var (
|
|||
func GetVersion() string {
|
||||
return version
|
||||
}
|
||||
|
||||
// GetCommit returns the git commit hash
|
||||
func GetCommit() string {
|
||||
return commit
|
||||
}
|
||||
|
||||
// GetBuildDate returns the build date
|
||||
func GetBuildDate() string {
|
||||
return date
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,71 +129,6 @@ func CreateMinerBundle(minerPath string, profileJSON []byte, name string, passwo
|
|||
}, 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.
|
||||
func ExtractProfileBundle(bundle *Bundle, password string) ([]byte, error) {
|
||||
// Verify checksum first
|
||||
|
|
@ -243,39 +178,6 @@ func ExtractMinerBundle(bundle *Bundle, password string, destDir string) (string
|
|||
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.
|
||||
func VerifyBundle(bundle *Bundle) bool {
|
||||
return calculateChecksum(bundle.Data) == bundle.Checksum
|
||||
|
|
|
|||
|
|
@ -111,6 +111,9 @@ func (c *Controller) sendRequest(peerID string, msg *Message, timeout time.Durat
|
|||
// GetRemoteStats requests miner statistics from a remote peer.
|
||||
func (c *Controller) GetRemoteStats(peerID string) (*StatsPayload, error) {
|
||||
identity := c.node.GetIdentity()
|
||||
if identity == nil {
|
||||
return nil, fmt.Errorf("node identity not initialized")
|
||||
}
|
||||
|
||||
msg, err := NewMessage(MsgGetStats, identity.ID, peerID, 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.
|
||||
func (c *Controller) StartRemoteMiner(peerID, profileID string, configOverride json.RawMessage) error {
|
||||
identity := c.node.GetIdentity()
|
||||
if identity == nil {
|
||||
return fmt.Errorf("node identity not initialized")
|
||||
}
|
||||
|
||||
payload := StartMinerPayload{
|
||||
ProfileID: profileID,
|
||||
|
|
@ -188,6 +194,9 @@ func (c *Controller) StartRemoteMiner(peerID, profileID string, configOverride j
|
|||
// StopRemoteMiner requests a remote peer to stop a miner.
|
||||
func (c *Controller) StopRemoteMiner(peerID, minerName string) error {
|
||||
identity := c.node.GetIdentity()
|
||||
if identity == nil {
|
||||
return fmt.Errorf("node identity not initialized")
|
||||
}
|
||||
|
||||
payload := StopMinerPayload{
|
||||
MinerName: minerName,
|
||||
|
|
@ -230,6 +239,9 @@ func (c *Controller) StopRemoteMiner(peerID, minerName string) error {
|
|||
// GetRemoteLogs requests console logs from a remote miner.
|
||||
func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]string, error) {
|
||||
identity := c.node.GetIdentity()
|
||||
if identity == nil {
|
||||
return nil, fmt.Errorf("node identity not initialized")
|
||||
}
|
||||
|
||||
payload := GetLogsPayload{
|
||||
MinerName: minerName,
|
||||
|
|
@ -266,51 +278,6 @@ func (c *Controller) GetRemoteLogs(peerID, minerName string, lines int) ([]strin
|
|||
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.
|
||||
func (c *Controller) GetAllStats() map[string]*StatsPayload {
|
||||
peers := c.peers.GetConnectedPeers()
|
||||
|
|
@ -336,26 +303,12 @@ func (c *Controller) GetAllStats() map[string]*StatsPayload {
|
|||
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.
|
||||
func (c *Controller) PingPeer(peerID string) (float64, error) {
|
||||
identity := c.node.GetIdentity()
|
||||
if identity == nil {
|
||||
return 0, fmt.Errorf("node identity not initialized")
|
||||
}
|
||||
sentAt := time.Now()
|
||||
|
||||
payload := PingPayload{
|
||||
|
|
|
|||
|
|
@ -171,16 +171,6 @@ func (n *NodeManager) DeriveSharedSecret(peerPubKeyBase64 string) ([]byte, error
|
|||
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.
|
||||
func (n *NodeManager) savePrivateKey() error {
|
||||
// Ensure directory exists
|
||||
|
|
@ -249,32 +239,6 @@ func (n *NodeManager) loadIdentity() error {
|
|||
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.
|
||||
func (n *NodeManager) Delete() error {
|
||||
n.mu.Lock()
|
||||
|
|
|
|||
|
|
@ -168,6 +168,9 @@ func TestPeerRegistry_UpdateMetrics(t *testing.T) {
|
|||
}
|
||||
|
||||
updated := pr.GetPeer("metrics-test")
|
||||
if updated == nil {
|
||||
t.Fatal("expected peer to exist")
|
||||
}
|
||||
if updated.PingMS != 50.5 {
|
||||
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")
|
||||
if updated == nil {
|
||||
t.Fatal("expected peer to exist")
|
||||
}
|
||||
if updated.Score != 85.5 {
|
||||
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")
|
||||
if updated == nil {
|
||||
t.Fatal("expected peer to exist")
|
||||
}
|
||||
if updated.Score != 100 {
|
||||
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")
|
||||
if updated == nil {
|
||||
t.Fatal("expected peer to exist")
|
||||
}
|
||||
if updated.Score != 0 {
|
||||
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)
|
||||
|
||||
updated := pr.GetPeer("connect-test")
|
||||
if updated == nil {
|
||||
t.Fatal("expected peer to exist")
|
||||
}
|
||||
if !updated.Connected {
|
||||
t.Error("peer should be connected")
|
||||
}
|
||||
|
|
@ -248,6 +263,9 @@ func TestPeerRegistry_SetConnected(t *testing.T) {
|
|||
|
||||
pr.SetConnected("connect-test", false)
|
||||
updated = pr.GetPeer("connect-test")
|
||||
if updated == nil {
|
||||
t.Fatal("expected peer to exist")
|
||||
}
|
||||
if updated.Connected {
|
||||
t.Error("peer should be disconnected")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -327,6 +327,10 @@ func (t *Transport) handleWSUpgrade(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// Send handshake acknowledgment
|
||||
identity := t.node.GetIdentity()
|
||||
if identity == nil {
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
ackPayload := HandshakeAckPayload{
|
||||
Identity: *identity,
|
||||
Accepted: true,
|
||||
|
|
@ -380,6 +384,9 @@ func (t *Transport) performHandshake(pc *PeerConnection) error {
|
|||
}()
|
||||
|
||||
identity := t.node.GetIdentity()
|
||||
if identity == nil {
|
||||
return fmt.Errorf("node identity not initialized")
|
||||
}
|
||||
|
||||
payload := HandshakePayload{
|
||||
Identity: *identity,
|
||||
|
|
|
|||
|
|
@ -83,14 +83,17 @@ func (w *Worker) HandleMessage(conn *PeerConnection, msg *Message) {
|
|||
|
||||
if err != nil {
|
||||
// Send error response
|
||||
errMsg, _ := NewErrorMessage(
|
||||
w.node.GetIdentity().ID,
|
||||
msg.From,
|
||||
ErrCodeOperationFailed,
|
||||
err.Error(),
|
||||
msg.ID,
|
||||
)
|
||||
conn.Send(errMsg)
|
||||
identity := w.node.GetIdentity()
|
||||
if identity != nil {
|
||||
errMsg, _ := NewErrorMessage(
|
||||
identity.ID,
|
||||
msg.From,
|
||||
ErrCodeOperationFailed,
|
||||
err.Error(),
|
||||
msg.ID,
|
||||
)
|
||||
conn.Send(errMsg)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -122,6 +125,9 @@ func (w *Worker) handlePing(msg *Message) (*Message, error) {
|
|||
// handleGetStats responds with current miner statistics.
|
||||
func (w *Worker) handleGetStats(msg *Message) (*Message, error) {
|
||||
identity := w.node.GetIdentity()
|
||||
if identity == nil {
|
||||
return nil, fmt.Errorf("node identity not initialized")
|
||||
}
|
||||
|
||||
stats := StatsPayload{
|
||||
NodeID: identity.ID,
|
||||
|
|
|
|||
|
|
@ -127,6 +127,9 @@ func TestWorker_HandlePing(t *testing.T) {
|
|||
|
||||
// Create a ping message
|
||||
identity := nm.GetIdentity()
|
||||
if identity == nil {
|
||||
t.Fatal("expected identity to be generated")
|
||||
}
|
||||
pingPayload := PingPayload{SentAt: time.Now().UnixMilli()}
|
||||
pingMsg, err := NewMessage(MsgPing, "sender-id", identity.ID, pingPayload)
|
||||
if err != nil {
|
||||
|
|
@ -183,6 +186,9 @@ func TestWorker_HandleGetStats(t *testing.T) {
|
|||
|
||||
// Create a get_stats message
|
||||
identity := nm.GetIdentity()
|
||||
if identity == nil {
|
||||
t.Fatal("expected identity to be generated")
|
||||
}
|
||||
msg, err := NewMessage(MsgGetStats, "sender-id", identity.ID, nil)
|
||||
if err != nil {
|
||||
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
|
||||
identity := nm.GetIdentity()
|
||||
if identity == nil {
|
||||
t.Fatal("expected identity to be generated")
|
||||
}
|
||||
payload := StartMinerPayload{ProfileID: "test-profile"}
|
||||
msg, err := NewMessage(MsgStartMiner, "sender-id", identity.ID, payload)
|
||||
if err != nil {
|
||||
|
|
@ -273,6 +282,9 @@ func TestWorker_HandleStopMiner_NoManager(t *testing.T) {
|
|||
|
||||
// Create a stop_miner message
|
||||
identity := nm.GetIdentity()
|
||||
if identity == nil {
|
||||
t.Fatal("expected identity to be generated")
|
||||
}
|
||||
payload := StopMinerPayload{MinerName: "test-miner"}
|
||||
msg, err := NewMessage(MsgStopMiner, "sender-id", identity.ID, payload)
|
||||
if err != nil {
|
||||
|
|
@ -308,6 +320,9 @@ func TestWorker_HandleGetLogs_NoManager(t *testing.T) {
|
|||
|
||||
// Create a get_logs message
|
||||
identity := nm.GetIdentity()
|
||||
if identity == nil {
|
||||
t.Fatal("expected identity to be generated")
|
||||
}
|
||||
payload := GetLogsPayload{MinerName: "test-miner", Lines: 100}
|
||||
msg, err := NewMessage(MsgGetLogs, "sender-id", identity.ID, payload)
|
||||
if err != nil {
|
||||
|
|
@ -343,6 +358,9 @@ func TestWorker_HandleDeploy_Profile(t *testing.T) {
|
|||
|
||||
// Create a deploy message for profile
|
||||
identity := nm.GetIdentity()
|
||||
if identity == nil {
|
||||
t.Fatal("expected identity to be generated")
|
||||
}
|
||||
payload := DeployPayload{
|
||||
BundleType: "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
|
||||
identity := nm.GetIdentity()
|
||||
if identity == nil {
|
||||
t.Fatal("expected identity to be generated")
|
||||
}
|
||||
payload := DeployPayload{
|
||||
BundleType: "unknown",
|
||||
Data: []byte(`{}`),
|
||||
|
|
|
|||
46
qodana.yaml
Normal file
46
qodana.yaml
Normal 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
|
||||
Loading…
Add table
Reference in a new issue