153 lines
4.6 KiB
Go
153 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Supply is one power supply the kernel reports.
|
|
type Supply struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Online *bool `json:"online,omitempty"`
|
|
Percent *int `json:"percent,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Device bool `json:"device,omitempty"` // a peripheral's battery (a mouse), not the machine's
|
|
Capacity string `json:"capacity_level,omitempty"`
|
|
}
|
|
|
|
// Supplies is every power supply under /sys/class/power_supply, by name.
|
|
func (m *Machine) Supplies() []Supply {
|
|
var out []Supply
|
|
for _, dir := range m.glob("/sys/class/power_supply/*") {
|
|
s := Supply{Name: filepath.Base(dir), Type: m.read(dir + "/type")}
|
|
if v, ok := m.readInt(dir + "/online"); ok {
|
|
on := v == 1
|
|
s.Online = &on
|
|
}
|
|
if v, ok := m.readInt(dir + "/capacity"); ok {
|
|
p := int(v)
|
|
s.Percent = &p
|
|
}
|
|
s.Status = m.read(dir + "/status")
|
|
s.Device = m.read(dir+"/scope") == "Device"
|
|
s.Capacity = m.read(dir + "/capacity_level")
|
|
out = append(out, s)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out
|
|
}
|
|
|
|
// Source is where the machine draws its power from: on-mains, on-battery, or none when it has no
|
|
// Mains supply to say (a desktop, a server, a USB-C source only) — the same reading as the runner's.
|
|
func Source(supplies []Supply) string {
|
|
found := false
|
|
for _, s := range supplies {
|
|
if s.Type != "Mains" {
|
|
continue
|
|
}
|
|
found = true
|
|
if s.Online != nil && *s.Online {
|
|
return "on-mains"
|
|
}
|
|
}
|
|
if found {
|
|
return "on-battery"
|
|
}
|
|
return "none"
|
|
}
|
|
|
|
// Battery is the machine's own battery charge, or nil when it has none.
|
|
func Battery(supplies []Supply) (percent *int, status string) {
|
|
for _, s := range supplies {
|
|
if s.Type == "Battery" && !s.Device && s.Percent != nil {
|
|
return s.Percent, s.Status
|
|
}
|
|
}
|
|
return nil, ""
|
|
}
|
|
|
|
// BootTime is when the kernel started, from /proc/stat's btime.
|
|
func (m *Machine) BootTime() (time.Time, bool) {
|
|
for _, line := range strings.Split(m.read("/proc/stat"), "\n") {
|
|
if f := strings.Fields(line); len(f) == 2 && f[0] == "btime" {
|
|
if v, err := strconv.ParseInt(f[1], 10, 64); err == nil {
|
|
return time.Unix(v, 0).UTC(), true
|
|
}
|
|
}
|
|
}
|
|
return time.Time{}, false
|
|
}
|
|
|
|
// Lid is the lid's state ("open", "closed"), or "" on a machine without one.
|
|
func (m *Machine) Lid() string {
|
|
for _, p := range m.glob("/proc/acpi/button/lid/*/state") {
|
|
if f := strings.Fields(m.read(p)); len(f) > 0 {
|
|
return f[len(f)-1]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// LogindSettings is what logind's power keys are set to, the last file to say each one winning, in
|
|
// logind's own order: its main file, then the drop-ins by name.
|
|
func (m *Machine) LogindSettings() map[string]map[string]string {
|
|
keys := []string{"HandlePowerKey", "HandleSuspendKey", "HandleHibernateKey", "HandleLidSwitch",
|
|
"HandleLidSwitchExternalPower", "HandleLidSwitchDocked", "IdleAction", "InhibitDelayMaxSec"}
|
|
want := map[string]bool{}
|
|
for _, k := range keys {
|
|
want[k] = true
|
|
}
|
|
out := map[string]map[string]string{}
|
|
files := append([]string{"/etc/systemd/logind.conf"}, sortedGlob(m, "/usr/lib/systemd/logind.conf.d/*.conf")...)
|
|
files = append(files, sortedGlob(m, "/etc/systemd/logind.conf.d/*.conf")...)
|
|
for _, f := range files {
|
|
for _, line := range strings.Split(m.read(f), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
k, v, ok := strings.Cut(line, "=")
|
|
if !ok || strings.HasPrefix(line, "#") || !want[strings.TrimSpace(k)] {
|
|
continue
|
|
}
|
|
out[strings.TrimSpace(k)] = map[string]string{"value": strings.TrimSpace(v), "from": f}
|
|
}
|
|
}
|
|
for _, k := range keys {
|
|
if _, set := out[k]; !set {
|
|
out[k] = map[string]string{"value": "(logind's default)", "from": ""}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func sortedGlob(m *Machine, pattern string) []string {
|
|
got := m.glob(pattern)
|
|
sort.Strings(got)
|
|
return got
|
|
}
|
|
|
|
// State is the machine's power now: since when it runs, where its power comes from, the battery and
|
|
// the lid, logind's settings, and what the module's watcher last saw and holds.
|
|
func (m *Machine) State(w *Watcher) map[string]any {
|
|
supplies := m.Supplies()
|
|
out := map[string]any{"source": Source(supplies), "supplies": orEmpty(supplies)}
|
|
if bt, ok := m.BootTime(); ok {
|
|
out["booted_at"] = bt.Format(time.RFC3339)
|
|
out["up_for"] = time.Since(bt).Round(time.Second).String()
|
|
}
|
|
if p, status := Battery(supplies); p != nil {
|
|
out["battery_percent"] = *p
|
|
out["battery_status"] = status
|
|
}
|
|
if lid := m.Lid(); lid != "" {
|
|
out["lid"] = lid
|
|
}
|
|
out["logind"] = m.LogindSettings()
|
|
if w != nil {
|
|
out["watcher"] = w.Snapshot()
|
|
}
|
|
return out
|
|
}
|