fix issues with detecting unmounted drives
This commit is contained in:
+179
-31
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 Name", disk.Info.Model))
|
||||||
term.Print(fmt.Sprintf("%-16s | %-16s", "Model Serial", disk.Info.Serial))
|
term.Print(fmt.Sprintf("%-16s | %-16s", "Model Serial", disk.Info.Serial))
|
||||||
term.Print(fmt.Sprintf("%-16s | %-16s", "Protocol", disk.Info.Protocol))
|
term.Print(fmt.Sprintf("%-16s | %-16s", "Protocol", disk.Info.Protocol))
|
||||||
|
|
||||||
var mp string
|
var mp string
|
||||||
if disk.Info.IsMounted {
|
if disk.Info.IsMounted {
|
||||||
mp = disk.Info.MountPoint
|
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))
|
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))
|
||||||
@@ -142,8 +144,33 @@ func GetDiskReport(devicePath string) (SMARTDiskReport, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var usage DiskUsage
|
var usage DiskUsage
|
||||||
|
|
||||||
if info.IsMounted {
|
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 {
|
if err == nil {
|
||||||
usage = *u
|
usage = *u
|
||||||
}
|
}
|
||||||
@@ -156,6 +183,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
|
||||||
@@ -183,17 +238,120 @@ func GetDiskInfo(devicePath string) (*DiskInfo, error) {
|
|||||||
Protocol: strings.ToUpper(extractProtocol(smartData)),
|
Protocol: strings.ToUpper(extractProtocol(smartData)),
|
||||||
}
|
}
|
||||||
|
|
||||||
mountPoint, isMounted, err := findMountPoint(devicePath)
|
mountPoints, isMounted, err := findAllMountPoints(devicePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to check mount status: %w", err)
|
return nil, fmt.Errorf("failed to check mount status: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
info.MountPoint = mountPoint
|
info.MountPoint = strings.Join(mountPoints, ", ")
|
||||||
info.IsMounted = isMounted
|
info.IsMounted = isMounted
|
||||||
|
|
||||||
return info, nil
|
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/<major:minor> 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) {
|
func GetDiskUsage(path string) (*DiskUsage, error) {
|
||||||
var stat syscall.Statfs_t
|
var stat syscall.Statfs_t
|
||||||
if err := syscall.Statfs(path, &stat); err != nil {
|
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") {
|
if protocol == "NVME" || strings.Contains(strings.ToLower(devicePath), "nvme") {
|
||||||
log := smartData.NVMeHealthLog
|
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.PowerOnHours = log.PowerOnHours
|
||||||
smart.PowerCycleCount = log.PowerCycles
|
smart.PowerCycleCount = log.PowerCycles
|
||||||
|
|
||||||
@@ -249,13 +414,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 +426,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 +444,7 @@ 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") {
|
if strings.HasPrefix(name, "loop") || strings.HasPrefix(name, "ram") || strings.HasPrefix(name, "sr") || strings.HasPrefix(name, "dm-") || strings.HasPrefix(name, "nbd") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,25 +457,6 @@ func GetBlockDevices() ([]string, error) {
|
|||||||
return devices, nil
|
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) {
|
func execSmartctl(devicePath string) (*smartctlJSON, error) {
|
||||||
cmd := exec.Command("smartctl", "-a", "-j", devicePath)
|
cmd := exec.Command("smartctl", "-a", "-j", devicePath)
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
@@ -320,6 +464,10 @@ func execSmartctl(devicePath string) (*smartctlJSON, error) {
|
|||||||
|
|
||||||
_ = 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)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
func NewIntelDrivers() ([]common.Driver, error) {
|
func NewIntelDrivers() ([]common.Driver, error) {
|
||||||
drivers := []common.Driver{}
|
drivers := []common.Driver{}
|
||||||
|
|
||||||
temperatureDriver, err := driver.NewHwmonDriver([]string{"coretemp"})
|
temperatureDriver, err := driver.NewHwmonDriver([]string{"coretemp"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not initialize hwmon temperature driver")
|
return nil, fmt.Errorf("could not initialize hwmon temperature driver")
|
||||||
|
|||||||
Reference in New Issue
Block a user