improve parsing of edge case smart power on times (#1347)

This commit is contained in:
henrygd
2025-10-30 16:32:06 -04:00
parent d0ff8ee2c0
commit 85169b6c5e
5 changed files with 182 additions and 55 deletions

View File

@@ -426,7 +426,7 @@ func (sm *SmartManager) smartctlArgs(deviceInfo *DeviceInfo, includeStandby bool
} }
} }
args = append(args, "-aj") args = append(args, "-a", "--json=c")
if includeStandby { if includeStandby {
args = append(args, "-n", "standby") args = append(args, "-n", "standby")
@@ -688,13 +688,17 @@ func (sm *SmartManager) parseSmartForSata(output []byte) (bool, int) {
// update SmartAttributes // update SmartAttributes
smartData.Attributes = make([]*smart.SmartAttribute, 0, len(data.AtaSmartAttributes.Table)) smartData.Attributes = make([]*smart.SmartAttribute, 0, len(data.AtaSmartAttributes.Table))
for _, attr := range data.AtaSmartAttributes.Table { for _, attr := range data.AtaSmartAttributes.Table {
rawValue := uint64(attr.Raw.Value)
if parsed, ok := smart.ParseSmartRawValueString(attr.Raw.String); ok {
rawValue = parsed
}
smartAttr := &smart.SmartAttribute{ smartAttr := &smart.SmartAttribute{
ID: attr.ID, ID: attr.ID,
Name: attr.Name, Name: attr.Name,
Value: attr.Value, Value: attr.Value,
Worst: attr.Worst, Worst: attr.Worst,
Threshold: attr.Thresh, Threshold: attr.Thresh,
RawValue: uint64(attr.Raw.Value), RawValue: rawValue,
RawString: attr.Raw.String, RawString: attr.Raw.String,
WhenFailed: attr.WhenFailed, WhenFailed: attr.WhenFailed,
} }

View File

@@ -89,6 +89,49 @@ func TestParseSmartForSata(t *testing.T) {
} }
} }
func TestParseSmartForSataParentheticalRawValue(t *testing.T) {
jsonPayload := []byte(`{
"smartctl": {"exit_status": 0},
"device": {"name": "/dev/sdz", "type": "sat"},
"model_name": "Example",
"serial_number": "PARENTHESES123",
"firmware_version": "1.0",
"user_capacity": {"bytes": 1024},
"smart_status": {"passed": true},
"temperature": {"current": 25},
"ata_smart_attributes": {
"table": [
{
"id": 9,
"name": "Power_On_Hours",
"value": 93,
"worst": 55,
"thresh": 0,
"when_failed": "",
"raw": {
"value": 57891864217128,
"string": "39925 (212 206 0)"
}
}
]
}
}`)
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
hasData, exitStatus := sm.parseSmartForSata(jsonPayload)
require.True(t, hasData)
assert.Equal(t, 0, exitStatus)
data, ok := sm.SmartDataMap["PARENTHESES123"]
require.True(t, ok)
require.Len(t, data.Attributes, 1)
attr := data.Attributes[0]
assert.Equal(t, uint64(39925), attr.RawValue)
assert.Equal(t, "39925 (212 206 0)", attr.RawString)
}
func TestParseSmartForNvme(t *testing.T) { func TestParseSmartForNvme(t *testing.T) {
fixturePath := filepath.Join("test-data", "smart", "nvme0.json") fixturePath := filepath.Join("test-data", "smart", "nvme0.json")
data, err := os.ReadFile(fixturePath) data, err := os.ReadFile(fixturePath)
@@ -198,7 +241,7 @@ func TestSmartctlArgsWithoutType(t *testing.T) {
sm := &SmartManager{} sm := &SmartManager{}
args := sm.smartctlArgs(device, true) args := sm.smartctlArgs(device, true)
assert.Equal(t, []string{"-aj", "-n", "standby", "/dev/sda"}, args) assert.Equal(t, []string{"-a", "--json=c", "-n", "standby", "/dev/sda"}, args)
} }
func TestSmartctlArgs(t *testing.T) { func TestSmartctlArgs(t *testing.T) {
@@ -206,17 +249,17 @@ func TestSmartctlArgs(t *testing.T) {
sataDevice := &DeviceInfo{Name: "/dev/sda", Type: "sat"} sataDevice := &DeviceInfo{Name: "/dev/sda", Type: "sat"}
assert.Equal(t, assert.Equal(t,
[]string{"-d", "sat", "-aj", "-n", "standby", "/dev/sda"}, []string{"-d", "sat", "-a", "--json=c", "-n", "standby", "/dev/sda"},
sm.smartctlArgs(sataDevice, true), sm.smartctlArgs(sataDevice, true),
) )
assert.Equal(t, assert.Equal(t,
[]string{"-d", "sat", "-aj", "/dev/sda"}, []string{"-d", "sat", "-a", "--json=c", "/dev/sda"},
sm.smartctlArgs(sataDevice, false), sm.smartctlArgs(sataDevice, false),
) )
assert.Equal(t, assert.Equal(t,
[]string{"-aj", "-n", "standby"}, []string{"-a", "--json=c", "-n", "standby"},
sm.smartctlArgs(nil, true), sm.smartctlArgs(nil, true),
) )
} }

View File

@@ -1,6 +1,7 @@
package smart package smart
import ( import (
"encoding/json"
"strconv" "strconv"
"strings" "strings"
) )
@@ -160,6 +161,33 @@ type RawValue struct {
String string `json:"string"` String string `json:"string"`
} }
func (r *RawValue) UnmarshalJSON(data []byte) error {
var tmp struct {
Value json.RawMessage `json:"value"`
String string `json:"string"`
}
if err := json.Unmarshal(data, &tmp); err != nil {
return err
}
if len(tmp.Value) > 0 {
if err := r.Value.UnmarshalJSON(tmp.Value); err != nil {
return err
}
} else {
r.Value = 0
}
r.String = tmp.String
if parsed, ok := ParseSmartRawValueString(tmp.String); ok {
r.Value = SmartRawValue(parsed)
}
return nil
}
type SmartRawValue uint64 type SmartRawValue uint64
// handles when drives report strings like "0h+0m+0.000s" or "7344 (253d 8h)" for power on hours // handles when drives report strings like "0h+0m+0.000s" or "7344 (253d 8h)" for power on hours
@@ -170,61 +198,73 @@ func (v *SmartRawValue) UnmarshalJSON(data []byte) error {
return nil return nil
} }
if trimmed[0] != '"' { if trimmed[0] == '"' {
parsed, err := strconv.ParseUint(trimmed, 0, 64) valueStr, err := strconv.Unquote(trimmed)
if err != nil { if err != nil {
return err return err
} }
*v = SmartRawValue(parsed) parsed, ok := ParseSmartRawValueString(valueStr)
return nil if ok {
} *v = SmartRawValue(parsed)
return nil
valueStr, err := strconv.Unquote(trimmed) }
if err != nil {
return err
}
if valueStr == "" {
*v = 0 *v = 0
return nil return nil
} }
if parsed, err := strconv.ParseUint(valueStr, 0, 64); err == nil { if parsed, err := strconv.ParseUint(trimmed, 0, 64); err == nil {
*v = SmartRawValue(parsed) *v = SmartRawValue(parsed)
return nil return nil
} }
if idx := strings.IndexRune(valueStr, 'h'); idx >= 0 { if parsed, ok := ParseSmartRawValueString(trimmed); ok {
hoursPart := strings.TrimSpace(valueStr[:idx]) *v = SmartRawValue(parsed)
if hoursPart == "" { return nil
*v = 0
return nil
}
if parsed, err := strconv.ParseFloat(hoursPart, 64); err == nil {
*v = SmartRawValue(uint64(parsed))
return nil
}
}
if digits := leadingDigitPrefix(valueStr); digits != "" {
if parsed, err := strconv.ParseUint(digits, 0, 64); err == nil {
*v = SmartRawValue(parsed)
return nil
}
} }
*v = 0 *v = 0
return nil return nil
} }
func leadingDigitPrefix(value string) string { // ParseSmartRawValueString attempts to extract a numeric value from the raw value
var builder strings.Builder // strings emitted by smartctl, which sometimes include human-friendly annotations
for _, r := range value { // like "7344 (253d 8h)" or "0h+0m+0.000s". It returns the parsed value and a
if r < '0' || r > '9' { // boolean indicating success.
break func ParseSmartRawValueString(value string) (uint64, bool) {
} value = strings.TrimSpace(value)
builder.WriteRune(r) if value == "" {
return 0, false
} }
return builder.String()
if parsed, err := strconv.ParseUint(value, 0, 64); err == nil {
return parsed, true
}
if idx := strings.IndexRune(value, 'h'); idx > 0 {
hoursPart := strings.TrimSpace(value[:idx])
if hoursPart != "" {
if parsed, err := strconv.ParseFloat(hoursPart, 64); err == nil {
return uint64(parsed), true
}
}
}
for i := 0; i < len(value); i++ {
if value[i] < '0' || value[i] > '9' {
continue
}
end := i + 1
for end < len(value) && value[end] >= '0' && value[end] <= '9' {
end++
}
digits := value[i:end]
if parsed, err := strconv.ParseUint(digits, 10, 64); err == nil {
return parsed, true
}
i = end
}
return 0, false
} }
// type PowerOnTimeInfo struct { // type PowerOnTimeInfo struct {

View File

@@ -3,28 +3,60 @@ package smart
import ( import (
"encoding/json" "encoding/json"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestSmartRawValueUnmarshalDuration(t *testing.T) { func TestSmartRawValueUnmarshalDuration(t *testing.T) {
input := []byte(`{"value":"62312h+33m+50.907s","string":"62312h+33m+50.907s"}`) input := []byte(`{"value":"62312h+33m+50.907s","string":"62312h+33m+50.907s"}`)
var raw RawValue var raw RawValue
if err := json.Unmarshal(input, &raw); err != nil { err := json.Unmarshal(input, &raw)
t.Fatalf("unexpected error unmarshalling raw value: %v", err) assert.NoError(t, err)
}
if uint64(raw.Value) != 62312 { assert.EqualValues(t, 62312, raw.Value)
t.Fatalf("expected hours to be 62312, got %d", raw.Value)
}
} }
func TestSmartRawValueUnmarshalNumericString(t *testing.T) { func TestSmartRawValueUnmarshalNumericString(t *testing.T) {
input := []byte(`{"value":"7344","string":"7344"}`) input := []byte(`{"value":"7344","string":"7344"}`)
var raw RawValue var raw RawValue
if err := json.Unmarshal(input, &raw); err != nil { err := json.Unmarshal(input, &raw)
t.Fatalf("unexpected error unmarshalling numeric string: %v", err) assert.NoError(t, err)
}
if uint64(raw.Value) != 7344 { assert.EqualValues(t, 7344, raw.Value)
t.Fatalf("expected hours to be 7344, got %d", raw.Value) }
}
func TestSmartRawValueUnmarshalParenthetical(t *testing.T) {
input := []byte(`{"value":"39925 (212 206 0)","string":"39925 (212 206 0)"}`)
var raw RawValue
err := json.Unmarshal(input, &raw)
assert.NoError(t, err)
assert.EqualValues(t, 39925, raw.Value)
}
func TestSmartRawValueUnmarshalDurationWithFractions(t *testing.T) {
input := []byte(`{"value":"2748h+31m+49.560s","string":"2748h+31m+49.560s"}`)
var raw RawValue
err := json.Unmarshal(input, &raw)
assert.NoError(t, err)
assert.EqualValues(t, 2748, raw.Value)
}
func TestSmartRawValueUnmarshalParentheticalRawValue(t *testing.T) {
input := []byte(`{"value":57891864217128,"string":"39925 (212 206 0)"}`)
var raw RawValue
err := json.Unmarshal(input, &raw)
assert.NoError(t, err)
assert.EqualValues(t, 39925, raw.Value)
}
func TestSmartRawValueUnmarshalDurationRawValue(t *testing.T) {
input := []byte(`{"value":57891864217128,"string":"2748h+31m+49.560s"}`)
var raw RawValue
err := json.Unmarshal(input, &raw)
assert.NoError(t, err)
assert.EqualValues(t, 2748, raw.Value)
} }

View File

@@ -1,3 +1,11 @@
## 0.15.3
- Improve parsing of edge case S.M.A.R.T. power on times. (#1347)
## 0.15.2
- Improve S.M.A.R.T. device detection logic (fix regression in 0.15.1) (#1345)
## 0.15.1 ## 0.15.1
- Add `SMART_DEVICES` environment variable to specify devices and types. (#373, #1335) - Add `SMART_DEVICES` environment variable to specify devices and types. (#373, #1335)