Files
Beach/internal/beach/smart.go
T
2026-10-27 16:57:53 -06:00

86 lines
2.1 KiB
Go

package beach
import (
"context"
"encoding/json"
"os/exec"
"sort"
"time"
)
// parseSmartctl reads `smartctl -j -H -i -A` output for one device.
func parseSmartctl(b []byte) (Disk, error) {
var r struct {
Device struct {
Name string `json:"name"`
} `json:"device"`
ModelName string `json:"model_name"`
SerialNumber string `json:"serial_number"`
UserCapacity struct {
Bytes int64 `json:"bytes"`
} `json:"user_capacity"`
SmartStatus struct {
Passed *bool `json:"passed"`
} `json:"smart_status"`
Temperature struct {
Current float64 `json:"current"`
} `json:"temperature"`
}
if err := json.Unmarshal(b, &r); err != nil {
return Disk{}, err
}
d := Disk{Name: r.Device.Name, Model: r.ModelName, Serial: r.SerialNumber, Size: r.UserCapacity.Bytes, TempC: r.Temperature.Current, Source: "smartctl"}
if r.SmartStatus.Passed != nil {
d.Health = "FAILED"
if *r.SmartStatus.Passed {
d.Health = "PASSED"
}
}
return d, nil
}
// runner lets tests replace the smartctl binary.
type runner func(ctx context.Context, args ...string) ([]byte, error)
func execSmartctl(ctx context.Context, args ...string) ([]byte, error) {
// smartctl uses non-zero exit bits to report problems; the JSON is still valid
out, err := exec.CommandContext(ctx, "smartctl", args...).Output()
if len(out) > 0 {
return out, nil
}
return out, err
}
// LocalSmart scans for devices and reads each one.
func LocalSmart(ctx context.Context, run runner) ([]Disk, error) {
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
b, err := run(ctx, "--scan", "-j")
if err != nil {
return nil, err
}
var scan struct {
Devices []struct {
Name string `json:"name"`
} `json:"devices"`
}
if err := json.Unmarshal(b, &scan); err != nil {
return nil, err
}
out := []Disk{}
for _, dev := range scan.Devices {
b, err := run(ctx, "-j", "-H", "-i", "-A", dev.Name)
if err != nil {
continue
}
if d, err := parseSmartctl(b); err == nil {
if d.Name == "" {
d.Name = dev.Name
}
out = append(out, d)
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out, nil
}