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).
185 lines
7.3 KiB
Go
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 ""
|
|
}
|