terrible smart driver
This commit is contained in:
@@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"finally-a-monolithic-linux-hw-monitor/app/common"
|
||||
"finally-a-monolithic-linux-hw-monitor/app/disks"
|
||||
"finally-a-monolithic-linux-hw-monitor/app/mem"
|
||||
"finally-a-monolithic-linux-hw-monitor/app/proc"
|
||||
"fmt"
|
||||
@@ -28,6 +29,7 @@ func Run() {
|
||||
term := common.NewTerminal()
|
||||
proc, _ := proc.GetProc()
|
||||
mem, _ := mem.GetMem()
|
||||
disks, _ := disks.GetDisks()
|
||||
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
@@ -36,6 +38,7 @@ func Run() {
|
||||
|
||||
proc.Render(term)
|
||||
mem.Render(term)
|
||||
disks.Render(term)
|
||||
|
||||
term.End()
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package disks
|
||||
|
||||
import (
|
||||
"finally-a-monolithic-linux-hw-monitor/app/common"
|
||||
"finally-a-monolithic-linux-hw-monitor/app/disks/drivers"
|
||||
)
|
||||
|
||||
type Disks struct {
|
||||
drivers []common.Driver
|
||||
}
|
||||
|
||||
func GetDisks() (Disks, error) {
|
||||
disks := Disks{}
|
||||
disks.drivers = []common.Driver{}
|
||||
|
||||
smart, err := drivers.NewSMARTDriver()
|
||||
if err == nil {
|
||||
disks.drivers = append(disks.drivers, smart)
|
||||
}
|
||||
|
||||
return disks, nil
|
||||
}
|
||||
|
||||
func (mem *Disks) Render(term common.Terminal) {
|
||||
term.Header("Disks")
|
||||
for _, driver := range mem.drivers {
|
||||
driver.Render(term)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package drivers
|
||||
|
||||
type DiskInfo struct {
|
||||
DevicePath string
|
||||
Model string
|
||||
Serial string
|
||||
Protocol string
|
||||
MountPoint string
|
||||
IsMounted bool
|
||||
}
|
||||
|
||||
type DiskUsage struct {
|
||||
TotalBytes uint64
|
||||
FreeBytes uint64
|
||||
UsedBytes uint64
|
||||
UsagePercent float64
|
||||
}
|
||||
|
||||
type DiskSMART struct {
|
||||
Passed bool
|
||||
TemperatureC int
|
||||
PowerOnHours int
|
||||
PowerCycleCount int
|
||||
WearoutPercent int
|
||||
HealthRemaining int
|
||||
HasWearData bool
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
package drivers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"finally-a-monolithic-linux-hw-monitor/app/common"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type SMARTDiskReport struct {
|
||||
Info DiskInfo
|
||||
Usage DiskUsage
|
||||
SMART DiskSMART
|
||||
}
|
||||
|
||||
// Internal JSON mapping matching smartctl 7.x+ output schemas
|
||||
type smartctlJSON struct {
|
||||
Device struct {
|
||||
Protocol string `json:"protocol"`
|
||||
ModelName string `json:"model_name"`
|
||||
} `json:"device"`
|
||||
Smartctl struct {
|
||||
ExitStatus int `json:"exit_status"`
|
||||
Messages []string `json:"messages"`
|
||||
} `json:"smartctl"`
|
||||
SmartStatus struct {
|
||||
Passed bool `json:"passed"`
|
||||
} `json:"smart_status"`
|
||||
|
||||
// ATA / SATA Root Metrics
|
||||
PowerOnTime struct {
|
||||
Hours int `json:"hours"`
|
||||
} `json:"power_on_time"`
|
||||
PowerCycleCount int `json:"power_cycle_count"`
|
||||
Temperature struct {
|
||||
Current int `json:"current"`
|
||||
} `json:"temperature"`
|
||||
ModelName string `json:"model_name"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Protocol string `json:"protocol"`
|
||||
|
||||
// NVMe-Specific Health Log Metrics
|
||||
NVMeHealthLog struct {
|
||||
PercentageUsed int `json:"percentage_used"`
|
||||
Temperature int `json:"temperature"`
|
||||
PowerOnHours int `json:"power_on_hours"`
|
||||
PowerCycles int `json:"power_cycles"`
|
||||
} `json:"nvme_smart_health_information_log"`
|
||||
|
||||
ATAMetrics struct {
|
||||
Table []struct {
|
||||
ID int `json:"id"`
|
||||
Value int `json:"value"`
|
||||
} `json:"table"`
|
||||
} `json:"ata_smart_attributes"`
|
||||
}
|
||||
|
||||
type SMARTDriver struct{}
|
||||
|
||||
func NewSMARTDriver() (*SMARTDriver, error) {
|
||||
r := SMARTDriver{}
|
||||
_, err := r.Read()
|
||||
return &r, err
|
||||
}
|
||||
|
||||
// Helper function to convert raw byte counts to decimal Gigabytes (GB)
|
||||
func formatBytesToGB(bytes uint64) string {
|
||||
gb := float64(bytes) / (1000 * 1000 * 1000)
|
||||
return fmt.Sprintf("%.2f GB", gb)
|
||||
}
|
||||
|
||||
func (r *SMARTDriver) Render(term common.Terminal) error {
|
||||
d, err := r.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
disks := d.([]SMARTDiskReport)
|
||||
term.Subheader("Disks")
|
||||
|
||||
for _, disk := range disks {
|
||||
term.Print(fmt.Sprintf(" Disk %s ", disk.Info.DevicePath))
|
||||
term.Print(fmt.Sprintf("%-16s | %-16s", "Model Name", disk.Info.Model))
|
||||
term.Print(fmt.Sprintf("%-16s | %-16s", "Model Serial", disk.Info.Serial))
|
||||
term.Print(fmt.Sprintf("%-16s | %-16s", "Protocol", disk.Info.Protocol))
|
||||
var mp string
|
||||
if disk.Info.IsMounted {
|
||||
mp = disk.Info.MountPoint
|
||||
} else {
|
||||
mp = "Unmounted"
|
||||
}
|
||||
term.Print(fmt.Sprintf("%-16s | %-16s", "Mount Point", mp))
|
||||
|
||||
// Formatted bytes in GB
|
||||
term.Print(fmt.Sprintf("%-16s | %-16s", "Total Capacity", formatBytesToGB(disk.Usage.TotalBytes)))
|
||||
term.Print(fmt.Sprintf("%-16s | %-16s", "Free Space", formatBytesToGB(disk.Usage.FreeBytes)))
|
||||
term.Print(fmt.Sprintf("%-16s | %-16s (%.2f%%)", "Used Space", formatBytesToGB(disk.Usage.UsedBytes), disk.Usage.UsagePercent))
|
||||
|
||||
term.Print(fmt.Sprintf("%-16s | %t", "SMART Passed", disk.SMART.Passed))
|
||||
term.Print(fmt.Sprintf("%-16s | %d%%", "Wearout", disk.SMART.WearoutPercent))
|
||||
term.Print(fmt.Sprintf("%-16s | %d%%", "Health Remaining", disk.SMART.HealthRemaining))
|
||||
term.Print(fmt.Sprintf("%-16s | %d C", "Temperature", disk.SMART.TemperatureC))
|
||||
term.Print(fmt.Sprintf("%-16s | %d hrs", "Power-On Hours", disk.SMART.PowerOnHours))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SMARTDriver) Read() (any, error) {
|
||||
devicePaths, err := GetBlockDevices()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
disks := []SMARTDiskReport{}
|
||||
|
||||
for _, devicePath := range devicePaths {
|
||||
disk, err := GetDiskReport(devicePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
disks = append(disks, disk)
|
||||
}
|
||||
|
||||
return disks, nil
|
||||
}
|
||||
|
||||
func GetDiskReport(devicePath string) (SMARTDiskReport, error) {
|
||||
info, err := GetDiskInfo(devicePath)
|
||||
if err != nil {
|
||||
return SMARTDiskReport{}, fmt.Errorf("failed to get disk info for %s: %w", devicePath, err)
|
||||
}
|
||||
|
||||
smart, err := GetDiskSMART(devicePath)
|
||||
if err != nil {
|
||||
return SMARTDiskReport{}, fmt.Errorf("failed to get SMART data for %s: %w", devicePath, err)
|
||||
}
|
||||
|
||||
var usage DiskUsage
|
||||
if info.IsMounted {
|
||||
u, err := GetDiskUsage(info.MountPoint)
|
||||
if err == nil {
|
||||
usage = *u
|
||||
}
|
||||
}
|
||||
|
||||
return SMARTDiskReport{
|
||||
Info: *info,
|
||||
Usage: usage,
|
||||
SMART: *smart,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func extractProtocol(smartData *smartctlJSON) string {
|
||||
if smartData.Protocol != "" {
|
||||
return smartData.Protocol
|
||||
}
|
||||
return smartData.Device.Protocol
|
||||
}
|
||||
|
||||
func extractModel(smartData *smartctlJSON) string {
|
||||
if smartData.ModelName != "" {
|
||||
return smartData.ModelName
|
||||
}
|
||||
return smartData.Device.ModelName
|
||||
}
|
||||
|
||||
func GetDiskInfo(devicePath string) (*DiskInfo, error) {
|
||||
smartData, err := execSmartctl(devicePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query device info: %w", err)
|
||||
}
|
||||
|
||||
info := &DiskInfo{
|
||||
DevicePath: devicePath,
|
||||
Model: extractModel(smartData),
|
||||
Serial: smartData.SerialNumber,
|
||||
Protocol: strings.ToUpper(extractProtocol(smartData)),
|
||||
}
|
||||
|
||||
mountPoint, isMounted, err := findMountPoint(devicePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check mount status: %w", err)
|
||||
}
|
||||
|
||||
info.MountPoint = mountPoint
|
||||
info.IsMounted = isMounted
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func GetDiskUsage(path string) (*DiskUsage, error) {
|
||||
var stat syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &stat); err != nil {
|
||||
return nil, fmt.Errorf("statfs failed for path %s: %w", path, err)
|
||||
}
|
||||
|
||||
total := stat.Blocks * uint64(stat.Bsize)
|
||||
free := stat.Bfree * uint64(stat.Bsize)
|
||||
used := total - free
|
||||
|
||||
var percent float64
|
||||
if total > 0 {
|
||||
percent = (float64(used) / float64(total)) * 100.0
|
||||
}
|
||||
|
||||
return &DiskUsage{
|
||||
TotalBytes: total,
|
||||
FreeBytes: free,
|
||||
UsedBytes: used,
|
||||
UsagePercent: percent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetDiskSMART(devicePath string) (*DiskSMART, error) {
|
||||
smartData, err := execSmartctl(devicePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch SMART data: %w", err)
|
||||
}
|
||||
|
||||
smart := &DiskSMART{
|
||||
Passed: smartData.SmartStatus.Passed,
|
||||
}
|
||||
|
||||
protocol := strings.ToUpper(extractProtocol(smartData))
|
||||
|
||||
if protocol == "NVME" || strings.Contains(strings.ToLower(devicePath), "nvme") {
|
||||
log := smartData.NVMeHealthLog
|
||||
|
||||
smart.TemperatureC = log.Temperature
|
||||
smart.PowerOnHours = log.PowerOnHours
|
||||
smart.PowerCycleCount = log.PowerCycles
|
||||
|
||||
smart.WearoutPercent = log.PercentageUsed
|
||||
smart.HealthRemaining = 100 - log.PercentageUsed
|
||||
if smart.HealthRemaining < 0 {
|
||||
smart.HealthRemaining = 0
|
||||
}
|
||||
smart.HasWearData = true
|
||||
} else {
|
||||
smart.TemperatureC = smartData.Temperature.Current
|
||||
smart.PowerOnHours = smartData.PowerOnTime.Hours
|
||||
smart.PowerCycleCount = smartData.PowerCycleCount
|
||||
|
||||
for _, attr := range smartData.ATAMetrics.Table {
|
||||
switch attr.ID {
|
||||
// Attributes that represent Health Remaining (100 -> 0)
|
||||
case 231, 177, 233, 169, 230, 173:
|
||||
smart.HealthRemaining = attr.Value
|
||||
smart.WearoutPercent = max(100-attr.Value, 0)
|
||||
smart.HasWearData = true
|
||||
|
||||
// Attributes that represent Percent Used / Wearout (0 -> 100+)
|
||||
case 202:
|
||||
smart.WearoutPercent = attr.Value
|
||||
smart.HealthRemaining = max(100-attr.Value, 0)
|
||||
smart.HasWearData = true
|
||||
}
|
||||
|
||||
if smart.HasWearData {
|
||||
break // Exits the for-loop once a matching attribute is found
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return smart, nil
|
||||
}
|
||||
|
||||
func GetBlockDevices() ([]string, error) {
|
||||
entries, err := os.ReadDir("/sys/block")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var devices []string
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
|
||||
if strings.HasPrefix(name, "loop") || strings.HasPrefix(name, "ram") || strings.HasPrefix(name, "sr") {
|
||||
continue
|
||||
}
|
||||
|
||||
devPath := filepath.Join("/dev", name)
|
||||
if _, err := os.Stat(devPath); err == nil {
|
||||
devices = append(devices, devPath)
|
||||
}
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func findMountPoint(devicePath string) (string, bool, error) {
|
||||
data, err := os.ReadFile("/proc/mounts")
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
if fields[0] == devicePath || strings.HasPrefix(fields[0], devicePath) {
|
||||
return fields[1], true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
func execSmartctl(devicePath string) (*smartctlJSON, error) {
|
||||
cmd := exec.Command("smartctl", "-a", "-j", devicePath)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
|
||||
_ = cmd.Run()
|
||||
|
||||
var result smartctlJSON
|
||||
if err := json.Unmarshal(out.Bytes(), &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse smartctl json output for %s: %w", devicePath, err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
+7
-7
@@ -5,13 +5,6 @@ import (
|
||||
"finally-a-monolithic-linux-hw-monitor/app/mem/drivers"
|
||||
)
|
||||
|
||||
func (mem *Mem) Render(term common.Terminal) {
|
||||
term.Header("Memory")
|
||||
for _, driver := range mem.drivers {
|
||||
driver.Render(term)
|
||||
}
|
||||
}
|
||||
|
||||
type Mem struct {
|
||||
drivers []common.Driver
|
||||
}
|
||||
@@ -32,3 +25,10 @@ func GetMem() (Mem, error) {
|
||||
|
||||
return mem, nil
|
||||
}
|
||||
|
||||
func (mem *Mem) Render(term common.Terminal) {
|
||||
term.Header("Memory")
|
||||
for _, driver := range mem.drivers {
|
||||
driver.Render(term)
|
||||
}
|
||||
}
|
||||
|
||||
+15
-15
@@ -8,24 +8,17 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (proc *Proc) Render(term common.Terminal) {
|
||||
term.Header(proc.Model())
|
||||
for _, driver := range proc.drivers {
|
||||
driver.Render(term)
|
||||
}
|
||||
}
|
||||
|
||||
type Proc struct {
|
||||
drivers []common.Driver
|
||||
Info map[string]string
|
||||
}
|
||||
|
||||
const (
|
||||
VendorIntel string = "GenuineIntel"
|
||||
VendorAMD string = "AuthenticAMD"
|
||||
VendorUnknown string = "Unknown"
|
||||
)
|
||||
|
||||
type Proc struct {
|
||||
drivers []common.Driver
|
||||
Info map[string]string
|
||||
}
|
||||
|
||||
func GetProc() (Proc, error) {
|
||||
// open and parse cpuinfo
|
||||
proc := Proc{}
|
||||
@@ -56,7 +49,7 @@ func GetProc() (Proc, error) {
|
||||
}
|
||||
}
|
||||
|
||||
vendor := proc.Vendor()
|
||||
vendor := proc.vendor()
|
||||
|
||||
switch vendor {
|
||||
case VendorIntel:
|
||||
@@ -72,10 +65,17 @@ func GetProc() (Proc, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (proc *Proc) Vendor() string {
|
||||
func (proc *Proc) Render(term common.Terminal) {
|
||||
term.Header(proc.model())
|
||||
for _, driver := range proc.drivers {
|
||||
driver.Render(term)
|
||||
}
|
||||
}
|
||||
|
||||
func (proc *Proc) vendor() string {
|
||||
return proc.Info["vendor_id"]
|
||||
}
|
||||
|
||||
func (proc *Proc) Model() string {
|
||||
func (proc *Proc) model() string {
|
||||
return proc.Info["model name"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user