fix issues with detecting unmounted drives

This commit is contained in:
alu
2026-08-10 10:13:27 -07:00
parent 683091902f
commit 26abd96a0b
+66 -11
View File
@@ -10,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"syscall" "syscall"
"unsafe"
) )
type SMARTDiskReport struct { type SMARTDiskReport struct {
@@ -47,7 +48,7 @@ type smartctlJSON struct {
// NVMe-Specific Health Log Metrics // NVMe-Specific Health Log Metrics
NVMeHealthLog struct { NVMeHealthLog struct {
PercentageUsed int `json:"percentage_used"` PercentageUsed int `json:"percentage_used"`
Temperature int `json:"temperature"` Temperature int `json:"temperature"` // Reported in Kelvin by smartctl NVMe JSON
PowerOnHours int `json:"power_on_hours"` PowerOnHours int `json:"power_on_hours"`
PowerCycles int `json:"power_cycles"` PowerCycles int `json:"power_cycles"`
} `json:"nvme_smart_health_information_log"` } `json:"nvme_smart_health_information_log"`
@@ -68,9 +69,10 @@ func NewSMARTDriver() (*SMARTDriver, error) {
return &r, err return &r, err
} }
// Helper function to convert raw byte counts to decimal Gigabytes (GB) const BytesPerGByte = 1000 * 1000 * 1000
func formatBytesToGB(bytes uint64) string { func formatBytesToGB(bytes uint64) string {
gb := float64(bytes) / (1000 * 1000 * 1000) gb := float64(bytes) / BytesPerGByte
return fmt.Sprintf("%.2f GB", gb) return fmt.Sprintf("%.2f GB", gb)
} }
@@ -92,11 +94,10 @@ func (r *SMARTDriver) Render(term common.Terminal) error {
if disk.Info.IsMounted { if disk.Info.IsMounted {
mp = disk.Info.MountPoint mp = disk.Info.MountPoint
} else { } else {
mp = "Unmounted" mp = "Unmounted (Raw/OSD/ZFS)"
} }
term.Print(fmt.Sprintf("%-16s | %-16s", "Mount Point", mp)) 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", "Total Capacity", formatBytesToGB(disk.Usage.TotalBytes)))
term.Print(fmt.Sprintf("%-16s | %-16s", "Free Space", formatBytesToGB(disk.Usage.FreeBytes))) 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 | %-16s (%.2f%%)", "Used Space", formatBytesToGB(disk.Usage.UsedBytes), disk.Usage.UsagePercent))
@@ -147,6 +148,11 @@ func GetDiskReport(devicePath string) (SMARTDiskReport, error) {
if err == nil { if err == nil {
usage = *u usage = *u
} }
} else {
u, err := GetRawBlockDeviceUsage(devicePath)
if err == nil {
usage = *u
}
} }
return SMARTDiskReport{ return SMARTDiskReport{
@@ -156,6 +162,34 @@ func GetDiskReport(devicePath string) (SMARTDiskReport, error) {
}, nil }, nil
} }
func GetRawBlockDeviceUsage(devicePath string) (*DiskUsage, error) {
file, err := os.Open(devicePath)
if err != nil {
return nil, fmt.Errorf("failed to open device %s: %w", devicePath, err)
}
defer file.Close()
var size uint64
const BLKGETSIZE64 = 0x80081272
_, _, sysErr := syscall.Syscall(
syscall.SYS_IOCTL,
file.Fd(),
uintptr(BLKGETSIZE64),
uintptr(unsafe.Pointer(&size)),
)
if sysErr != 0 {
return nil, fmt.Errorf("ioctl BLKGETSIZE64 failed: %v", sysErr)
}
return &DiskUsage{
TotalBytes: size,
FreeBytes: 0,
UsedBytes: 0,
UsagePercent: 0.0,
}, nil
}
func extractProtocol(smartData *smartctlJSON) string { func extractProtocol(smartData *smartctlJSON) string {
if smartData.Protocol != "" { if smartData.Protocol != "" {
return smartData.Protocol return smartData.Protocol
@@ -232,7 +266,15 @@ func GetDiskSMART(devicePath string) (*DiskSMART, error) {
if protocol == "NVME" || strings.Contains(strings.ToLower(devicePath), "nvme") { if protocol == "NVME" || strings.Contains(strings.ToLower(devicePath), "nvme") {
log := smartData.NVMeHealthLog log := smartData.NVMeHealthLog
smart.TemperatureC = log.Temperature // Convert Kelvin to Celsius if smartctl raw NVMe log provides Kelvin (> 200)
tempC := log.Temperature
if smartData.Temperature.Current > 0 {
tempC = smartData.Temperature.Current
} else if tempC > 200 {
tempC -= 273
}
smart.TemperatureC = tempC
smart.PowerOnHours = log.PowerOnHours smart.PowerOnHours = log.PowerOnHours
smart.PowerCycleCount = log.PowerCycles smart.PowerCycleCount = log.PowerCycles
@@ -249,13 +291,11 @@ func GetDiskSMART(devicePath string) (*DiskSMART, error) {
for _, attr := range smartData.ATAMetrics.Table { for _, attr := range smartData.ATAMetrics.Table {
switch attr.ID { switch attr.ID {
// Attributes that represent Health Remaining (100 -> 0)
case 231, 177, 233, 169, 230, 173: case 231, 177, 233, 169, 230, 173:
smart.HealthRemaining = attr.Value smart.HealthRemaining = attr.Value
smart.WearoutPercent = max(100-attr.Value, 0) smart.WearoutPercent = max(100-attr.Value, 0)
smart.HasWearData = true smart.HasWearData = true
// Attributes that represent Percent Used / Wearout (0 -> 100+)
case 202: case 202:
smart.WearoutPercent = attr.Value smart.WearoutPercent = attr.Value
smart.HealthRemaining = max(100-attr.Value, 0) smart.HealthRemaining = max(100-attr.Value, 0)
@@ -263,7 +303,7 @@ func GetDiskSMART(devicePath string) (*DiskSMART, error) {
} }
if smart.HasWearData { if smart.HasWearData {
break // Exits the for-loop once a matching attribute is found break
} }
} }
} }
@@ -281,7 +321,8 @@ func GetBlockDevices() ([]string, error) {
for _, entry := range entries { for _, entry := range entries {
name := entry.Name() name := entry.Name()
if strings.HasPrefix(name, "loop") || strings.HasPrefix(name, "ram") || strings.HasPrefix(name, "sr") { // Ignore virtual and non-disk devices
if strings.HasPrefix(name, "loop") || strings.HasPrefix(name, "ram") || strings.HasPrefix(name, "sr") || strings.HasPrefix(name, "dm-") || strings.HasPrefix(name, "nbd") {
continue continue
} }
@@ -295,6 +336,12 @@ func GetBlockDevices() ([]string, error) {
} }
func findMountPoint(devicePath string) (string, bool, error) { func findMountPoint(devicePath string) (string, bool, error) {
// Resolve symlinks (e.g., /dev/disk/by-id/...) to raw dev node
resolvedPath, err := filepath.EvalSymlinks(devicePath)
if err != nil {
resolvedPath = devicePath
}
data, err := os.ReadFile("/proc/mounts") data, err := os.ReadFile("/proc/mounts")
if err != nil { if err != nil {
return "", false, err return "", false, err
@@ -304,7 +351,9 @@ func findMountPoint(devicePath string) (string, bool, error) {
for _, line := range lines { for _, line := range lines {
fields := strings.Fields(line) fields := strings.Fields(line)
if len(fields) >= 2 { if len(fields) >= 2 {
if fields[0] == devicePath || strings.HasPrefix(fields[0], devicePath) { mountedDev := fields[0]
// Strict match to avoid matching /dev/sda when /dev/sda1 is mounted
if mountedDev == resolvedPath {
return fields[1], true, nil return fields[1], true, nil
} }
} }
@@ -318,8 +367,14 @@ func execSmartctl(devicePath string) (*smartctlJSON, error) {
var out bytes.Buffer var out bytes.Buffer
cmd.Stdout = &out cmd.Stdout = &out
// smartctl uses bitmask exit codes (e.g., bit 1 set for SMART warnings).
// Run command and proceed if JSON output was produced regardless of exit status.
_ = cmd.Run() _ = cmd.Run()
if out.Len() == 0 {
return nil, fmt.Errorf("smartctl returned empty output for %s", devicePath)
}
var result smartctlJSON var result smartctlJSON
if err := json.Unmarshal(out.Bytes(), &result); err != nil { 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 nil, fmt.Errorf("failed to parse smartctl json output for %s: %w", devicePath, err)