Merge pull request 'Report whether a battery powers the machine and whether anything measures its draw (hq ADR 0255)' (#56) from feat/the-bar-takes-blocks into main

This commit was merged in pull request #56.
This commit is contained in:
2026-10-08 12:55:38 +00:00
3 changed files with 270 additions and 0 deletions
+2
View File
@@ -187,6 +187,8 @@ func Default(runner Runner) []Detector {
privileged{},
graphicalSession{},
seat{},
battery{},
powerMeter{},
commandCapability{
name: CapContainerRuntime, command: "docker", args: []string{"info", "--format", "{{.ServerVersion}}"},
why: "asks the daemon for its version — a running daemon, not an installed client",
+135
View File
@@ -0,0 +1,135 @@
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}
}
+133
View File
@@ -0,0 +1,133 @@
package profile
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
)
// Defends novox/hq ADR 0255: a machine says whether a battery powers it and whether anything on it
// measures the power it draws, read from the kernel, so the controller composes a battery's or a power
// draw's block only where there is one.
func write(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content+"\n"), 0o644); err != nil {
t.Fatal(err)
}
}
// sysfs points every power source at a directory of its own, empty unless a case fills it.
func sysfs(t *testing.T) string {
t.Helper()
dir := t.TempDir()
old := [4]string{PowerSupplySource, PowercapSource, SeatSource, NvidiaSource}
PowerSupplySource = filepath.Join(dir, "power_supply")
PowercapSource = filepath.Join(dir, "powercap")
SeatSource = filepath.Join(dir, "drm")
NvidiaSource = filepath.Join(dir, "nvidia")
t.Cleanup(func() { PowerSupplySource, PowercapSource, SeatSource, NvidiaSource = old[0], old[1], old[2], old[3] })
return dir
}
func TestALaptopHasABatteryAndAMeter(t *testing.T) {
dir := sysfs(t)
write(t, dir+"/power_supply/ACAD/type", "Mains")
write(t, dir+"/power_supply/BAT1/type", "Battery")
write(t, dir+"/power_supply/BAT1/scope", "System")
write(t, dir+"/power_supply/BAT1/present", "1")
write(t, dir+"/powercap/intel-rapl:0/name", "package-0")
write(t, dir+"/powercap/intel-rapl:0/energy_uj", "1")
b := battery{}.Detect(context.Background())
if !b.Present || b.Detail != "BAT1" {
t.Fatalf("battery: %+v", b)
}
m := powerMeter{}.Detect(context.Background())
if !m.Present || m.Detail != "battery BAT1, processor package-0" {
t.Fatalf("power meter: %+v", m)
}
}
func TestADevicesBatteryIsNotTheMachines(t *testing.T) {
// The case worth a test: a desktop with a wireless mouse reports a Battery, and a battery block
// composed for it would show the mouse's charge as the machine's.
dir := sysfs(t)
write(t, dir+"/power_supply/hidpp_battery_0/type", "Battery")
write(t, dir+"/power_supply/hidpp_battery_0/scope", "Device")
write(t, dir+"/power_supply/BAT0/type", "Battery")
write(t, dir+"/power_supply/BAT0/present", "0")
if b := (battery{}).Detect(context.Background()); b.Present {
t.Fatalf("a mouse's battery, or an empty bay, was taken for the machine's: %+v", b)
}
if m := (powerMeter{}).Detect(context.Background()); m.Present {
t.Fatalf("a device's battery was taken for a meter: %+v", m)
}
}
func TestADesktopMeasuresItsProcessorAndItsGraphics(t *testing.T) {
dir := sysfs(t)
write(t, dir+"/powercap/intel-rapl:0/name", "package-0")
write(t, dir+"/powercap/intel-rapl:0/energy_uj", "1")
// A sub-domain is part of the package, not a second meter.
write(t, dir+"/powercap/intel-rapl:0:0/name", "core")
write(t, dir+"/powercap/intel-rapl:0:0/energy_uj", "1")
write(t, dir+"/drm/card1/device/hwmon/hwmon4/power1_average", "8000000")
write(t, dir+"/drm/card1/device/mem_info_vram_vendor", "samsung")
write(t, dir+"/nvidia/0000:01:00.0/information", "x")
if b := (battery{}).Detect(context.Background()); b.Present {
t.Fatalf("a desktop has a battery: %+v", b)
}
m := powerMeter{}.Detect(context.Background())
if !m.Present || m.Detail != "processor package-0, graphics card1, graphics nvidia" {
t.Fatalf("power meter: %+v", m)
}
}
func TestAMachineWithNothingToReadHasNeither(t *testing.T) {
sysfs(t)
b, m := battery{}.Detect(context.Background()), powerMeter{}.Detect(context.Background())
if b.Present || m.Present || b.How == "" || m.How == "" || !strings.Contains(m.Detail, "nothing") {
t.Fatalf("%+v %+v", b, m)
}
}
func TestAPackageShownTwiceIsOneMeterAndIntegratedGraphicsAreNone(t *testing.T) {
dir := sysfs(t)
write(t, dir+"/powercap/intel-rapl:0/name", "package-0")
write(t, dir+"/powercap/intel-rapl:0/energy_uj", "1")
write(t, dir+"/powercap/intel-rapl-mmio:0/name", "package-0")
write(t, dir+"/powercap/intel-rapl-mmio:0/energy_uj", "1")
// An APU's graphics: power reported, no memory of its own.
write(t, dir+"/drm/card1/device/hwmon/hwmon5/power1_average", "19000000")
m := powerMeter{}.Detect(context.Background())
if !m.Present || m.Detail != "processor package-0" {
t.Fatalf("power meter: %+v", m)
}
}
func TestOnlyIntegratedGraphicsMeasureNothing(t *testing.T) {
dir := sysfs(t)
write(t, dir+"/drm/card0/device/hwmon/hwmon1/power1_input", "5000000")
if m := (powerMeter{}).Detect(context.Background()); m.Present {
t.Fatalf("integrated graphics were taken for a meter: %+v", m)
}
}
func TestABatteryOfUnknownScopeIsTheMachines(t *testing.T) {
// Many laptops' firmware says nothing of scope; the kernel reports Unknown. Only Device is a
// peripheral's.
dir := sysfs(t)
write(t, dir+"/power_supply/BAT0/type", "Battery")
write(t, dir+"/power_supply/BAT0/scope", "Unknown")
if b := (battery{}).Detect(context.Background()); !b.Present || b.Detail != "BAT0" {
t.Fatalf("battery: %+v", b)
}
}