124 lines
3.8 KiB
Go
124 lines
3.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// CommandTimeout bounds every command a tool runs: a manager that hangs on its bus must cost a tool
|
|
// call twenty seconds, never the runtime's thirty.
|
|
const CommandTimeout = 20 * time.Second
|
|
|
|
// OutputCap is the most a tool answers of one command's output.
|
|
const OutputCap = 16 * 1024
|
|
|
|
// The module's places on the machine (novox/hq ADR 0211): the runner it ships, and the files the
|
|
// controller fills with every module's code for each moment.
|
|
const (
|
|
RunnerPath = "/usr/local/lib/mesh-power/bin/power-moment"
|
|
MomentsDir = "/etc/mesh-power/moments"
|
|
LogindFile = "/etc/systemd/logind.conf.d/power.conf"
|
|
UdevRule = "/etc/udev/rules.d/90-mesh-power.rules"
|
|
)
|
|
|
|
// Moments are the power moments a module may contribute code for, in the order a machine meets them.
|
|
var Moments = []string{"after-boot", "on-mains", "on-battery", "before-sleep", "after-wake", "before-shutdown"}
|
|
|
|
// Units are the module's own units and the moment each runs.
|
|
var Units = map[string]string{
|
|
"mesh-power-after-boot.service": "after-boot",
|
|
"mesh-power-before-shutdown.service": "before-shutdown",
|
|
"mesh-power-before-sleep.service": "before-sleep",
|
|
"mesh-power-after-wake.service": "after-wake",
|
|
"mesh-power-supply.service": "on-mains / on-battery",
|
|
}
|
|
|
|
// Runner runs one command and answers its standard output. Injected, so every tool is tested against
|
|
// recorded answers rather than this machine's managers.
|
|
type Runner func(ctx context.Context, name string, args ...string) (string, error)
|
|
|
|
// ExecRunner runs a command, bounded by CommandTimeout. A failure carries what it said on stderr.
|
|
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()
|
|
out := capped(stdout.String())
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
return out, 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())
|
|
}
|
|
return out, fmt.Errorf("%s %s: %w: %s", name, strings.Join(args, " "), err, capped(said))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func capped(s string) string {
|
|
if len(s) <= OutputCap {
|
|
return s
|
|
}
|
|
return s[:OutputCap] + "\n… (cut at 16 KiB)"
|
|
}
|
|
|
|
// Machine is what the module reads and acts on: a filesystem root (the real one, or a test's tree)
|
|
// 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) }
|
|
|
|
func (m *Machine) read(p string) string {
|
|
b, err := os.ReadFile(m.path(p))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(b))
|
|
}
|
|
|
|
func (m *Machine) readInt(p string) (int64, bool) {
|
|
n, err := strconv.ParseInt(m.read(p), 10, 64)
|
|
return n, err == nil
|
|
}
|
|
|
|
func (m *Machine) exists(p string) bool {
|
|
_, err := os.Stat(m.path(p))
|
|
return err == nil
|
|
}
|
|
|
|
func (m *Machine) glob(pattern string) []string {
|
|
got, _ := filepath.Glob(m.path(pattern))
|
|
out := make([]string, 0, len(got))
|
|
for _, g := range got {
|
|
rel, err := filepath.Rel(m.Root, g)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
out = append(out, "/"+rel)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// privileged runs a command as root without asking for a password (sudo -n), as the module's other
|
|
// action tools do: the runtime runs as the operator's account, and these files are root's.
|
|
func (m *Machine) privileged(ctx context.Context, name string, args ...string) (string, error) {
|
|
return m.Run(ctx, "sudo", append([]string{"-n", name}, args...)...)
|
|
}
|