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)) // The touchpad reset after waking is this module's contribution to node-power's after-wake moment // (novox/hq ADR 0211), placed in the power module's moment file under a "# asus-zephyrus-g14" line. afterWake := m.read("/etc/mesh-power/moments/after-wake") placed := strings.Contains(afterWake, "# asus-zephyrus-g14\n") && strings.Contains(afterWake, "zephyrus-touchpad reset") add("touchpad after resume", placed, "the reset is placed in the power module's after-wake moment: %v (it needs the power module on this machine)", placed) if uid := m.triggerhappyUID(); uid < 0 { add("triggerhappy as the account", false, "no thd process runs: the vendor keys do nothing") } else { add("triggerhappy as the account", uid == operatorUID(), "thd runs as uid %d; the operator's account is %d (the module's drop-in passes --user)", uid, operatorUID()) } refs := m.asUserReferences() add("no as-user", len(refs) == 0, "%s", orNone(map[bool]string{true: "", false: "the predecessor's as-user is still named in " + strings.Join(refs, ", ") + ": it su-s to a named person on a guessed display and sources a file of secrets"}[len(refs) == 0])) 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", "scripts/media-control", "scripts/xinput-reset-touchpad", ".config/i3/scripts/kbd-brightness-notify.sh", "scripts/.screenlayouts/orden-workspaces.sh", } 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 "" }