Code around sleep was written into the service manager's sleep units by the module that needed it, and the mesh could not tell a sleeping machine from a lost one. power runs every module's code for the six moments, each piece bounded, owns logind's power handling from its settings, and says booted, sleeping, woke, shutting-down and the power source on the bus, sleeping under logind's delay lock before the machine sleeps.
204 lines
8.5 KiB
Go
204 lines
8.5 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))
|
|
|
|
// 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 ""
|
|
}
|