The second review of #129 found a read stuck in the kernel kept the file fresh and the module healthy, smartd as a second reader could wake or keep awake the drives the watch spares, and a board chip's maximum could stop a fan. Read each drive in its own goroutine from a root reader of the module's own, mark each read in flight, and say a read past its bound or a drive not looked at for twice its cadence; drop smartd; tell smartctl each drive's kind; take a SAS drive's standby; say a drive asleep since when.
133 lines
4.6 KiB
Go
133 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
// Fans, from the hwmon chips. Many fans stop on purpose when the machine is cool (a laptop at rest, a GPU
|
|
// in its zero-fan mode), so 0 RPM alone is not a fault; nor is a chip's minimum or alarm bit, which a
|
|
// zero-fan GPU is under by design and a Super I/O chip latches. Those are said in a note and raise nothing.
|
|
// A fan is STOPPED only when it ran since the machine started, reads 0 now, and a temperature that fan
|
|
// cools is at or above the hardware's own maximum: a CPU sensor's, a thermal zone's, or one on the fan's
|
|
// own chip unless that is a board chip (novox/hq ADR 0256 rule 4).
|
|
|
|
// FlagStopped is said beside a fan that should be turning and is not.
|
|
const FlagStopped = "STOPPED"
|
|
|
|
// Fan is one fan's reading.
|
|
type Fan struct {
|
|
Chip string `json:"chip"`
|
|
Label string `json:"label"`
|
|
// Key names the fan across readings: its chip and its input (asus/fan1).
|
|
Key string `json:"key"`
|
|
RPM *int64 `json:"rpm,omitempty"`
|
|
// Min is the chip's own lowest speed for it, where it gives one.
|
|
Min *int64 `json:"min_rpm,omitempty"`
|
|
Alarm bool `json:"alarm,omitempty"`
|
|
// RanSinceBoot says the module's watch saw it turning since the machine started; absent when the
|
|
// watch has not said.
|
|
RanSinceBoot *bool `json:"ran_since_boot,omitempty"`
|
|
Flag string `json:"flag,omitempty"`
|
|
Why string `json:"why,omitempty"`
|
|
// Note is what the chip says that raises nothing: under its own minimum, its alarm bit.
|
|
Note string `json:"note,omitempty"`
|
|
NotRead string `json:"not_read,omitempty"`
|
|
Where string `json:"where"`
|
|
}
|
|
|
|
// Name is how a person reads which fan this is.
|
|
func (f Fan) Name() string {
|
|
if f.Label != "" && !strings.HasPrefix(f.Label, "fan") {
|
|
return fmt.Sprintf("the fan %s (%s)", f.Label, f.Chip)
|
|
}
|
|
return fmt.Sprintf("the fan %s of %s", f.Label, f.Chip)
|
|
}
|
|
|
|
// Fans is every fan the machine reports, read now.
|
|
func (m *Machine) Fans() []Fan {
|
|
var out []Fan
|
|
for _, h := range m.numbered("/sys/class/hwmon", "hwmon") {
|
|
dir := "/sys/class/hwmon/" + h
|
|
chip := m.read(dir + "/name")
|
|
for _, stem := range m.inputs(dir, "fan") {
|
|
f := Fan{Chip: chip, Label: m.read(dir + "/" + stem + "_label"), Key: chip + "/" + stem, Where: dir + "/" + stem}
|
|
if f.Label == "" {
|
|
f.Label = stem
|
|
}
|
|
v, ok := m.readInt(dir + "/" + stem + "_input")
|
|
if !ok || v < 0 {
|
|
f.NotRead = "the fan gave no speed"
|
|
out = append(out, f)
|
|
continue
|
|
}
|
|
f.RPM = &v
|
|
if mn, ok := m.readInt(dir + "/" + stem + "_min"); ok && mn > 0 {
|
|
f.Min = &mn
|
|
}
|
|
if a, ok := m.readInt(dir + "/" + stem + "_alarm"); ok && a != 0 {
|
|
f.Alarm = true
|
|
}
|
|
out = append(out, f)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// judgeFans flags each fan, given which fans the watch saw turning since the machine started (nil when it
|
|
// has not said) and the temperatures read now.
|
|
func judgeFans(fans []Fan, ran map[string]bool, temps []Temperature) []Fan {
|
|
for i := range fans {
|
|
f := &fans[i]
|
|
if ran != nil {
|
|
r := ran[f.Key]
|
|
f.RanSinceBoot = &r
|
|
}
|
|
if f.RPM == nil {
|
|
continue
|
|
}
|
|
var notes []string
|
|
if f.Alarm {
|
|
notes = append(notes, fmt.Sprintf("its chip's alarm bit is set at %d RPM", *f.RPM))
|
|
}
|
|
if f.Min != nil && *f.RPM < *f.Min {
|
|
notes = append(notes, fmt.Sprintf("it turns at %d RPM, under its chip's minimum of %d RPM", *f.RPM, *f.Min))
|
|
}
|
|
f.Note = strings.Join(notes, "; ")
|
|
if *f.RPM != 0 || f.RanSinceBoot == nil || !*f.RanSinceBoot {
|
|
continue
|
|
}
|
|
if hot := hottestFor(*f, temps); hot != nil {
|
|
f.Flag, f.Why = FlagStopped, fmt.Sprintf("it ran earlier and reads 0 RPM while %s is at %.1f °C, at or above %s of %.1f °C",
|
|
hot.Name(), *hot.C, hot.HighIs, *hot.High)
|
|
}
|
|
}
|
|
return fans
|
|
}
|
|
|
|
// hottestFor is the temperature furthest at or above its own high value among those this fan may be
|
|
// cooling: a CPU sensor, a thermal zone (whose trips the firmware sets for the machine as a whole), or a
|
|
// sensor on the fan's own chip when that chip is not a board chip. A board chip's (a Super I/O's) limits
|
|
// are whatever the firmware left, so its maximum is no more trusted here than its critical value is for
|
|
// health. A drive or a network adapter running hot says nothing of a fan.
|
|
func hottestFor(f Fan, ts []Temperature) *Temperature {
|
|
chip := path.Dir(f.Where)
|
|
var best *Temperature
|
|
for i := range ts {
|
|
t := &ts[i]
|
|
if t.C == nil || t.High == nil || *t.C < *t.High {
|
|
continue
|
|
}
|
|
related := t.Kind == "cpu" || t.Source == "thermal zone" ||
|
|
(t.Source == "hwmon" && path.Dir(t.Where) == chip && kindOf(f.Chip) != "board")
|
|
if !related {
|
|
continue
|
|
}
|
|
if best == nil || *t.C-*t.High > *best.C-*best.High {
|
|
best = t
|
|
}
|
|
}
|
|
return best
|
|
}
|