From 413b79116ecc91cfe002851e793decc14e85c2da Mon Sep 17 00:00:00 2001 From: alu Date: Mon, 10 Aug 2026 10:13:27 -0700 Subject: [PATCH] fix issues with detecting unmounted drives --- app/disks/drivers/smart.go | 210 +++++++++++++++++++++++++++++++------ app/proc/vendor_intel.go | 1 + 2 files changed, 180 insertions(+), 31 deletions(-) diff --git a/app/disks/drivers/smart.go b/app/disks/drivers/smart.go index 8ccbf39..a9b0ff8 100644 --- a/app/disks/drivers/smart.go +++ b/app/disks/drivers/smart.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" "syscall" + "unsafe" ) type SMARTDiskReport struct { @@ -47,7 +48,7 @@ type smartctlJSON struct { // NVMe-Specific Health Log Metrics NVMeHealthLog struct { 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"` PowerCycles int `json:"power_cycles"` } `json:"nvme_smart_health_information_log"` @@ -68,9 +69,10 @@ func NewSMARTDriver() (*SMARTDriver, error) { return &r, err } -// Helper function to convert raw byte counts to decimal Gigabytes (GB) +const BytesPerGByte = 1000 * 1000 * 1000 + func formatBytesToGB(bytes uint64) string { - gb := float64(bytes) / (1000 * 1000 * 1000) + gb := float64(bytes) / BytesPerGByte return fmt.Sprintf("%.2f GB", gb) } @@ -88,6 +90,7 @@ func (r *SMARTDriver) Render(term common.Terminal) error { 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 @@ -96,7 +99,6 @@ func (r *SMARTDriver) Render(term common.Terminal) error { } 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)) @@ -142,8 +144,33 @@ func GetDiskReport(devicePath string) (SMARTDiskReport, error) { } var usage DiskUsage + if info.IsMounted { - u, err := GetDiskUsage(info.MountPoint) + mountPoints := strings.Split(info.MountPoint, ", ") + var total, free, used uint64 + + for _, mp := range mountPoints { + u, err := GetDiskUsage(mp) + if err == nil { + total += u.TotalBytes + free += u.FreeBytes + used += u.UsedBytes + } + } + + var percent float64 + if total > 0 { + percent = (float64(used) / float64(total)) * 100.0 + } + + usage = DiskUsage{ + TotalBytes: total, + FreeBytes: free, + UsedBytes: used, + UsagePercent: percent, + } + } else { + u, err := GetRawBlockDeviceUsage(devicePath) if err == nil { usage = *u } @@ -156,6 +183,34 @@ func GetDiskReport(devicePath string) (SMARTDiskReport, error) { }, 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 { if smartData.Protocol != "" { return smartData.Protocol @@ -183,17 +238,120 @@ func GetDiskInfo(devicePath string) (*DiskInfo, error) { Protocol: strings.ToUpper(extractProtocol(smartData)), } - mountPoint, isMounted, err := findMountPoint(devicePath) + mountPoints, isMounted, err := findAllMountPoints(devicePath) if err != nil { return nil, fmt.Errorf("failed to check mount status: %w", err) } - info.MountPoint = mountPoint + info.MountPoint = strings.Join(mountPoints, ", ") info.IsMounted = isMounted return info, nil } +func findAllMountPoints(devicePath string) ([]string, bool, error) { + diskDevNum, err := getDeviceNumber(devicePath) + if err != nil { + return nil, false, err + } + + // Map of device numbers (e.g. "8:0", "8:3", "252:6") belonging to this drive or its partitions/sub-devices + associatedDevNums := getAssociatedDeviceNumbers(diskDevNum) + + data, err := os.ReadFile("/proc/mounts") + if err != nil { + return nil, false, err + } + + var mountPoints []string + seenMounts := make(map[string]bool) + + lines := strings.Split(string(data), "\n") + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + + mountedDev := fields[0] + mountPoint := fields[1] + + // Skip non-device pseudo filesystems (sysfs, proc, tmpfs, etc.) + if !strings.HasPrefix(mountedDev, "/dev/") { + continue + } + + mountedDevNum, err := getDeviceNumber(mountedDev) + if err != nil { + continue + } + + if associatedDevNums[mountedDevNum] { + if !seenMounts[mountPoint] { + seenMounts[mountPoint] = true + mountPoints = append(mountPoints, mountPoint) + } + } + } + + return mountPoints, len(mountPoints) > 0, nil +} + +func getDeviceNumber(devPath string) (string, error) { + resolvedPath, err := filepath.EvalSymlinks(devPath) + if err != nil { + resolvedPath = devPath + } + + var stat syscall.Stat_t + if err := syscall.Stat(resolvedPath, &stat); err != nil { + return "", err + } + + rdev := uint64(stat.Rdev) + major := (rdev >> 8) & 0xfff + minor := (rdev & 0xff) | ((rdev >> 12) & 0xfff00) + + return fmt.Sprintf("%d:%d", major, minor), nil +} + +func getAssociatedDeviceNumbers(diskDevNum string) map[string]bool { + devNums := map[string]bool{diskDevNum: true} + + // Read /sys/dev/block/ to locate sysfs device node + sysDevPath, err := filepath.EvalSymlinks(filepath.Join("/sys/dev/block", diskDevNum)) + if err != nil { + return devNums + } + + // Recursively inspect sysfs entries for partition subdirectories and DM/LVM holders + var collectDevs func(dir string) + collectDevs = func(dir string) { + entries, err := os.ReadDir(dir) + if err != nil { + return + } + + for _, entry := range entries { + devFile := filepath.Join(dir, entry.Name(), "dev") + if devData, err := os.ReadFile(devFile); err == nil { + num := strings.TrimSpace(string(devData)) + if num != "" && !devNums[num] { + devNums[num] = true + // Recurse into sysfs holders subdirectory to follow LVM / Device Mapper relationships + collectDevs(filepath.Join(dir, entry.Name(), "holders")) + } + } + if entry.Name() == "holders" { + collectDevs(filepath.Join(dir, entry.Name())) + } + } + } + + collectDevs(sysDevPath) + return devNums +} + func GetDiskUsage(path string) (*DiskUsage, error) { var stat syscall.Statfs_t if err := syscall.Statfs(path, &stat); err != nil { @@ -232,7 +390,14 @@ func GetDiskSMART(devicePath string) (*DiskSMART, error) { if protocol == "NVME" || strings.Contains(strings.ToLower(devicePath), "nvme") { log := smartData.NVMeHealthLog - smart.TemperatureC = log.Temperature + 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.PowerCycleCount = log.PowerCycles @@ -249,13 +414,11 @@ func GetDiskSMART(devicePath string) (*DiskSMART, error) { 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) @@ -263,7 +426,7 @@ func GetDiskSMART(devicePath string) (*DiskSMART, error) { } if smart.HasWearData { - break // Exits the for-loop once a matching attribute is found + break } } } @@ -281,7 +444,7 @@ func GetBlockDevices() ([]string, error) { for _, entry := range entries { name := entry.Name() - if strings.HasPrefix(name, "loop") || strings.HasPrefix(name, "ram") || strings.HasPrefix(name, "sr") { + if strings.HasPrefix(name, "loop") || strings.HasPrefix(name, "ram") || strings.HasPrefix(name, "sr") || strings.HasPrefix(name, "dm-") || strings.HasPrefix(name, "nbd") { continue } @@ -294,25 +457,6 @@ func GetBlockDevices() ([]string, error) { 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 @@ -320,6 +464,10 @@ func execSmartctl(devicePath string) (*smartctlJSON, error) { _ = cmd.Run() + if out.Len() == 0 { + return nil, fmt.Errorf("smartctl returned empty output for %s", devicePath) + } + 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) diff --git a/app/proc/vendor_intel.go b/app/proc/vendor_intel.go index cc91541..92009f2 100644 --- a/app/proc/vendor_intel.go +++ b/app/proc/vendor_intel.go @@ -8,6 +8,7 @@ import ( func NewIntelDrivers() ([]common.Driver, error) { drivers := []common.Driver{} + temperatureDriver, err := driver.NewHwmonDriver([]string{"coretemp"}) if err != nil { return nil, fmt.Errorf("could not initialize hwmon temperature driver")