An APU's integrated graphics and Intel's second (MMIO) view of a package made a machine report a power meter the sampler would not read, so its bar could only say not measured.
136 lines
5.0 KiB
Go
136 lines
5.0 KiB
Go
package profile
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// What this machine has to say about its own power (novox/hq ADR 0255): whether it has a battery,
|
|
// and whether anything on it can measure the power it draws.
|
|
//
|
|
// **Hardware, read from the kernel, as `seat` is.** A module that shows a battery, or the power a
|
|
// machine draws, is on every machine — the power module holds node-power everywhere — and what it
|
|
// shows depends on what is there. The controller composes such a piece only where the machine
|
|
// reported the capability, so a desktop never gets a battery block that would sit empty, and the
|
|
// bar never asks at run time what the mesh could have said once.
|
|
|
|
// Named power capabilities.
|
|
const (
|
|
// CapBattery is a battery that powers this machine: a power supply of type Battery that is not
|
|
// a device's (a mouse's or a headset's battery has scope Device).
|
|
CapBattery = "battery"
|
|
// CapPowerMeter is something here that measures the power the machine, or part of it, draws:
|
|
// the machine's battery, the processor package's energy counter (powercap, Intel's and AMD's
|
|
// RAPL), or a discrete graphics device that reports its own power — the same meters the power
|
|
// module's sampler reads, so a machine never gets a block that can only say "not measured".
|
|
CapPowerMeter = "power-meter"
|
|
)
|
|
|
|
// Where the kernel reports supplies and energy counters. Variables so a test can point them at a
|
|
// directory it made, rather than at whatever this machine happens to have.
|
|
var (
|
|
PowerSupplySource = "/sys/class/power_supply"
|
|
PowercapSource = "/sys/class/powercap"
|
|
NvidiaSource = "/proc/driver/nvidia/gpus"
|
|
)
|
|
|
|
// systemBatteries is every battery that powers this machine, by name.
|
|
func systemBatteries() []string {
|
|
entries, err := os.ReadDir(PowerSupplySource)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var out []string
|
|
for _, e := range entries {
|
|
dir := filepath.Join(PowerSupplySource, e.Name())
|
|
if readTrimmed(filepath.Join(dir, "type")) != "Battery" {
|
|
continue
|
|
}
|
|
if readTrimmed(filepath.Join(dir, "scope")) == "Device" {
|
|
continue
|
|
}
|
|
// A bay with no battery in it still has a directory.
|
|
if readTrimmed(filepath.Join(dir, "present")) == "0" {
|
|
continue
|
|
}
|
|
out = append(out, e.Name())
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func readTrimmed(path string) string {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(b))
|
|
}
|
|
|
|
type battery struct{}
|
|
|
|
func (battery) Name() string { return CapBattery }
|
|
|
|
func (battery) Detect(context.Context) Verdict {
|
|
const how = "/sys/class/power_supply/*/type — a Battery whose scope is not Device"
|
|
if found := systemBatteries(); len(found) > 0 {
|
|
return Verdict{Name: CapBattery, Present: true, Detail: strings.Join(found, ", "), How: how}
|
|
}
|
|
return Verdict{Name: CapBattery, Present: false, Detail: "no battery powers this machine", How: how}
|
|
}
|
|
|
|
type powerMeter struct{}
|
|
|
|
func (powerMeter) Name() string { return CapPowerMeter }
|
|
|
|
// Detect names every meter it found. Only that one exists: whether its counter can be read is the
|
|
// reader's question — powercap's energy counter is root's to read, and the power module samples it
|
|
// as root and publishes only a rate.
|
|
func (powerMeter) Detect(context.Context) Verdict {
|
|
const how = "a system battery; /sys/class/powercap/*/name starting package, each name once; a discrete " +
|
|
"graphics device's (mem_info_vram_vendor) hwmon power1_average or power1_input; /proc/driver/nvidia/gpus"
|
|
var found []string
|
|
for _, b := range systemBatteries() {
|
|
found = append(found, "battery "+b)
|
|
}
|
|
// Each package once by its name: Intel shows some twice, as intel-rapl:N and intel-rapl-mmio:N.
|
|
if entries, err := os.ReadDir(PowercapSource); err == nil {
|
|
seen := map[string]bool{}
|
|
for _, e := range entries {
|
|
dir := filepath.Join(PowercapSource, e.Name())
|
|
name := readTrimmed(filepath.Join(dir, "name"))
|
|
if !strings.HasPrefix(name, "package") || seen[name] {
|
|
continue
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "energy_uj")); err == nil {
|
|
seen[name] = true
|
|
found = append(found, "processor "+name)
|
|
}
|
|
}
|
|
}
|
|
if cards, err := filepath.Glob(filepath.Join(SeatSource, "card*", "device", "hwmon", "hwmon*")); err == nil {
|
|
for _, hw := range cards {
|
|
// Integrated graphics report the package the processor's counter already holds.
|
|
if _, err := os.Stat(filepath.Join(filepath.Dir(filepath.Dir(hw)), "mem_info_vram_vendor")); err != nil {
|
|
continue
|
|
}
|
|
for _, f := range []string{"power1_average", "power1_input"} {
|
|
if _, err := os.Stat(filepath.Join(hw, f)); err == nil {
|
|
found = append(found, "graphics "+strings.Split(strings.TrimPrefix(hw, SeatSource+"/"), "/")[0])
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if gpus, err := os.ReadDir(NvidiaSource); err == nil && len(gpus) > 0 {
|
|
found = append(found, "graphics nvidia")
|
|
}
|
|
if len(found) == 0 {
|
|
return Verdict{Name: CapPowerMeter, Present: false, Detail: "nothing here measures the power drawn", How: how}
|
|
}
|
|
return Verdict{Name: CapPowerMeter, Present: true, Detail: strings.Join(found, ", "), How: how}
|
|
}
|