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).
125 lines
3.8 KiB
Go
125 lines
3.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// CommandTimeout bounds every command a tool or the switcher runs: a vendor daemon that hangs on its
|
|
// bus must cost a tool call twenty seconds, never the runtime's thirty.
|
|
const CommandTimeout = 20 * time.Second
|
|
|
|
// Runner runs one command and answers its standard output. It is injected so that every tool is
|
|
// tested against recorded answers rather than this machine's daemons.
|
|
type Runner func(ctx context.Context, name string, args ...string) (string, error)
|
|
|
|
// ExecRunner runs a command on the machine, bounded by CommandTimeout. A failure carries what the
|
|
// command said on stderr, because "exit status 1" names nothing.
|
|
func ExecRunner(ctx context.Context, name string, args ...string) (string, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, CommandTimeout)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, name, args...)
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
|
err := cmd.Run()
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
return stdout.String(), fmt.Errorf("%s did not answer within %s", name, CommandTimeout)
|
|
}
|
|
if err != nil {
|
|
said := strings.TrimSpace(stderr.String())
|
|
if said == "" {
|
|
said = strings.TrimSpace(stdout.String())
|
|
}
|
|
if said != "" {
|
|
return stdout.String(), fmt.Errorf("%s %s: %w: %s", name, strings.Join(args, " "), err, said)
|
|
}
|
|
return stdout.String(), fmt.Errorf("%s %s: %w", name, strings.Join(args, " "), err)
|
|
}
|
|
return stdout.String(), nil
|
|
}
|
|
|
|
// Machine is what the module reads and acts on: a filesystem root (the real one, or a test's tree of
|
|
// /sys and /proc and /etc) and a way to run commands.
|
|
type Machine struct {
|
|
Root string
|
|
Run Runner
|
|
}
|
|
|
|
// Here is the machine this process runs on.
|
|
func Here() *Machine { return &Machine{Root: "/", Run: ExecRunner} }
|
|
|
|
func (m *Machine) path(p string) string { return filepath.Join(m.Root, p) }
|
|
|
|
// read is a file's content, trimmed; "" when it cannot be read.
|
|
func (m *Machine) read(p string) string {
|
|
b, err := os.ReadFile(m.path(p))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(b))
|
|
}
|
|
|
|
// readInt is a file holding one integer; ok false when it is absent or not a number.
|
|
func (m *Machine) readInt(p string) (int64, bool) {
|
|
s := m.read(p)
|
|
if s == "" {
|
|
return 0, false
|
|
}
|
|
n, err := strconv.ParseInt(s, 10, 64)
|
|
return n, err == nil
|
|
}
|
|
|
|
func (m *Machine) glob(pattern string) []string {
|
|
found, _ := filepath.Glob(m.path(pattern))
|
|
out := make([]string, 0, len(found))
|
|
for _, f := range found {
|
|
rel, err := filepath.Rel(m.Root, f)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
out = append(out, "/"+filepath.ToSlash(rel))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// write puts a value into a file of the kernel's (a backlight). Where the account may not write it
|
|
// — the udev rule that gives the video group the panel has not run yet — it escalates with `sudo -n`,
|
|
// which never prompts: the operator's account may escalate without one, and when it may not, the
|
|
// tool says so in sudo's words.
|
|
func (m *Machine) write(ctx context.Context, p, value string) error {
|
|
err := os.WriteFile(m.path(p), []byte(value), 0)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if !errors.Is(err, os.ErrPermission) {
|
|
return err
|
|
}
|
|
if _, serr := m.Run(ctx, "sudo", "-n", "sh", "-c", `printf '%s' "$1" > "$2"`, "sh", value, m.path(p)); serr != nil {
|
|
return fmt.Errorf("%s is not writable by this account and sudo -n refused: %v", p, serr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// notInstalled says a command failed because it is not on this machine at all.
|
|
func notInstalled(err error) bool { return errors.Is(err, exec.ErrNotFound) }
|
|
|
|
// round to one decimal, for watts and percentages a person reads.
|
|
func round1(f float64) float64 {
|
|
return float64(int64(f*10+sign(f)*0.5)) / 10
|
|
}
|
|
|
|
func sign(f float64) float64 {
|
|
if f < 0 {
|
|
return -1
|
|
}
|
|
return 1
|
|
}
|