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} }