Files
mesh-catalog/modules/asus-zephyrus-g14/cmd/zephyrus/check.go
T
jochen 9c84cd9e6c Phase 3: asus-zephyrus-g14 and memory-pressure, with Go tools and long-running code
The laptop model's hardware module and a memory-pressure module for any
machine (hq research 027/03, 026/05, to-be 42 phase 3). The predecessor's
polling auto-profile and mem-guard user scripts become each module's own
Go code launched by the node runtime (ADR 0198): a profile switcher woken
by the kernel's power-supply uevents, and a guard that warns on RAM, swap
or PSI before systemd-oomd acts, on the desktop over the account's bus and
always as an event. supergfxctl and triggerhappy are kept as found
(research 027 Q1).
2026-10-04 12:56:23 +02:00

185 lines
7.3 KiB
Go

package main
import (
"context"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
)
// Check is one thing the module expects of the machine, and whether it holds.
type Check struct {
Name string `json:"name"`
OK bool `json:"ok"`
Detail string `json:"detail"`
}
// CheckReport is what zephyrus_check answers. NotChecked says what it did not look at, because a
// check that reads as clean while skipping something is the predecessor's verifier again.
type CheckReport struct {
Model string `json:"model"`
Checks []Check `json:"checks"`
Failing int `json:"failing"`
NotChecked []string `json:"not_checked"`
}
var (
pacmanVersion = regexp.MustCompile(`(?m)^Version\s*:\s*(\S+)`)
pacmanPackager = regexp.MustCompile(`(?m)^Packager\s*:\s*(.+)$`)
nvidiaParam = regexp.MustCompile(`(?m)^(\w+):\s*(\S+)`)
)
// ParseNvidiaParams reads /proc/driver/nvidia/params.
func ParseNvidiaParams(text string) map[string]string {
out := map[string]string{}
for _, g := range nvidiaParam.FindAllStringSubmatch(text, -1) {
out[g[1]] = g[2]
}
return out
}
// predecessorProcess finds a running process whose command line names the predecessor's script.
func (m *Machine) predecessorProcess(name string) (int, bool) {
for _, dir := range m.glob("/proc/[0-9]*") {
cmd := strings.ReplaceAll(m.read(dir+"/cmdline"), "\x00", " ")
if strings.Contains(cmd, "/"+name) && !strings.Contains(cmd, "zephyrus") {
var pid int
fmt.Sscanf(filepath.Base(dir), "%d", &pid)
return pid, true
}
}
return 0, false
}
func (m *Machine) unitIs(ctx context.Context, verb, unit string) string {
out, _ := m.Run(ctx, "systemctl", verb, unit)
return strings.TrimSpace(out)
}
// Check reads every expectation and reports each.
func (m *Machine) Check(ctx context.Context, sw *Switcher) CheckReport {
r := CheckReport{Model: m.Model(), NotChecked: []string{
"the fan curves (asusd's own, read them with zephyrus_fan_curves)",
"whether the initramfs carries the NVIDIA options (they are read from the running driver instead)",
"the vendor keys themselves (press them)",
}}
add := func(name string, ok bool, format string, a ...any) {
r.Checks = append(r.Checks, Check{Name: name, OK: ok, Detail: fmt.Sprintf(format, a...)})
if !ok {
r.Failing++
}
}
add("model", m.ThisModel(), "the firmware reports %q; this module is for %q", r.Model, ModelFamily)
// asusctl: present, and from the distribution rather than a local build.
if info, err := m.Run(ctx, "pacman", "-Qi", "asusctl"); err != nil {
add("asusctl package", false, "not installed: %v", err)
} else {
v, p := "", ""
if g := pacmanVersion.FindStringSubmatch(info); g != nil {
v = g[1]
}
if g := pacmanPackager.FindStringSubmatch(info); g != nil {
p = strings.TrimSpace(g[1])
}
local := p == "Unknown Packager"
add("asusctl package", !local, "version %s, packager %s%s", v, p,
map[bool]string{true: "; a local build — the distribution's package replaces it at the next upgrade (pacman -S asusctl)", false: ""}[local])
}
for _, foreign := range []string{"supergfxctl", "triggerhappy"} {
_, err := m.Run(ctx, "pacman", "-Q", foreign)
add(foreign+" package", err == nil, "%s; not in the distribution's repositories, kept as found (novox/hq research 027, question 1)",
map[bool]string{true: "installed", false: "NOT installed"}[err == nil])
}
for _, unit := range []string{"asusd.service", "supergfxd.service", "triggerhappy.service"} {
state := m.unitIs(ctx, "is-active", unit)
add(unit, state == "active", "%s", state)
}
powerd := m.unitIs(ctx, "is-enabled", "nvidia-powerd.service")
add("nvidia-powerd.service", powerd == "masked" || powerd == "disabled" || powerd == "" || strings.Contains(powerd, "not-found"),
"%s; the module's drop-in keeps it from starting unless the kernel command line says zephyrus.nvidia-powerd", orNone(powerd))
wants, _ := m.Run(ctx, "systemctl", "show", "-p", "Wants", "systemd-suspend.service")
add("nvidia suspend and resume", strings.Contains(wants, "nvidia-suspend.service") && strings.Contains(wants, "nvidia-resume.service"),
"systemd-suspend.service %s", strings.TrimSpace(wants))
params := ParseNvidiaParams(m.read("/proc/driver/nvidia/params"))
if len(params) == 0 {
add("nvidia options", false, "the NVIDIA driver is not loaded (no /proc/driver/nvidia/params)")
} else {
add("nvidia options", params["PreserveVideoMemoryAllocations"] == "1" && params["DynamicPowerManagement"] == "0",
"PreserveVideoMemoryAllocations=%s DynamicPowerManagement=%s (want 1 and 0; a change applies when the driver loads again)",
params["PreserveVideoMemoryAllocations"], params["DynamicPowerManagement"])
}
for _, b := range m.Batteries() {
ok := b.LimitPercent != nil && *b.LimitPercent == ChargeLimitPercent
have := "unknown"
if b.LimitPercent != nil {
have = fmt.Sprintf("%d%%", *b.LimitPercent)
}
add("charge limit", ok, "%s is %s, the module's is %d%%", b.Name, have, ChargeLimitPercent)
}
if c := m.Asusd(); c != nil && (c.ACCommand != "" || c.BatteryCommand != "") {
add("one authority over the GPU mode", false,
"asusd runs %q on mains and %q on battery: it switches the GPU mode on every change of power source, "+
"so a mode set with zephyrus_gpu_mode lasts until the next one. Clear ac_command and bat_command in /etc/asusd/asusd.ron (asusd's file) to make it the operator's alone",
c.ACCommand, c.BatteryCommand)
}
if out, err := m.Run(ctx, "journalctl", "-b", "-u", "supergfxd.service", "-g", "invalid variant", "-n", "1", "-q", "-o", "cat"); err == nil && strings.TrimSpace(out) != "" {
add("supergfxd and logind", false, "supergfxd cannot read logind's sessions this boot (%s): a mode change that needs a logout times out", strings.TrimSpace(out))
}
if pid, ok := m.predecessorProcess("auto-profile"); ok {
add("one profile switcher", false, "the predecessor's auto-profile still runs (pid %d) and switches the profile every five seconds; "+
"stop it: systemctl --user disable --now auto-profile.service", pid)
} else {
add("one profile switcher", true, "no predecessor auto-profile is running")
}
if sw != nil {
rep := sw.Report()
add("profile switcher", rep.Running, "%s", orNone(firstNonEmpty(rep.Disabled, rep.LastError, "woken by "+rep.Watching)))
}
if home := os.Getenv("MESH_OPERATOR_HOME"); home != "" {
var left []string
for _, p := range PredecessorHomeFiles {
if _, err := os.Stat(filepath.Join(m.Root, home, p)); err == nil {
left = append(left, "~/"+p)
}
}
add("predecessor files in the home", len(left) == 0, "%s", orNone(strings.Join(left, ", ")))
} else {
r.NotChecked = append(r.NotChecked, "the predecessor's files in the operator's home (MESH_OPERATOR_HOME is not set)")
}
return r
}
// PredecessorHomeFiles are what the predecessor placed in the operator's home for this model and this
// module replaces. The mesh removes nothing it did not make (novox/hq ADR 0182): the operator does,
// once, and this list is how the check knows.
var PredecessorHomeFiles = []string{
"scripts/auto-profile",
".config/systemd/user/auto-profile.service",
"scripts/asus-bright",
"scripts/asusctl-kbd-bright",
"scripts/xrandr-bright",
}
func orNone(s string) string {
if strings.TrimSpace(s) == "" {
return "none"
}
return s
}
func firstNonEmpty(ss ...string) string {
for _, s := range ss {
if s != "" {
return s
}
}
return ""
}