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).
107 lines
3.0 KiB
Go
107 lines
3.0 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 guard runs: a journal that takes long to search
|
|
// 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
|
|
// /proc, /sys 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
|
|
}
|
|
|
|
// 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
|
|
}
|