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 }