Each node's reflector ranked its own list on its own timer, and on 2026-10-31 one node's lagging first mirror left it a Node.js version behind the rest. The list is now the module's state mirrors, key list: the node that opens a week's round ranks it with the module's reflector options, keeps only mirrors whose last sync is no older than the newest found, and the round's key names the list's revision. Every keeper writes /etc/pacman.d/mirrorlist whole from the key through the root helper's new verb mirrorlist, when the key changes and when the file differs, and never writes an empty or unreadable list. reflector.timer is declared stopped and disabled, and reflector.conf saves nothing. pacman_mirrors answers the shared list, its ranker and age and whether this node's file matches; with refresh it asks this node's keeper to rank again, refused while a round runs, naming it. The round's integration test proves the opening write and both nodes' files. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
309 lines
9.7 KiB
Go
309 lines
9.7 KiB
Go
package main
|
|
|
|
// The commands this bundle runs on its machine, and who runs them.
|
|
//
|
|
// Who asks. The node's tool runtime runs as the operator account, not root (novox/hq ADR 0175 §4),
|
|
// and launches this binary as a process of its own (ADR 0188, ADR 0193) with the runtime's words —
|
|
// HOME, a PATH, MESH_OPERATOR_ACCOUNT — and no session words. Reading needs nothing more; what only
|
|
// root may do goes through `sudo -n`, as the packet filter's, the service manager's and the
|
|
// intrusion prevention's tools do (to-be 38 WP4), and the `sudo` module is what declares that the
|
|
// account may (to-be 42, research 027). A refusal is named by how it failed, never read as an
|
|
// empty answer.
|
|
//
|
|
// The runner is injected, so every tool is tested over a fake one without the machine.
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// Ran is what one command did: its output, its exit status, and why it never ran to an answer.
|
|
type Ran struct {
|
|
Stdout string
|
|
Stderr string
|
|
Status int
|
|
// Err is "ENOENT" when the program is not there, or that it was ended for taking too long.
|
|
Err string
|
|
}
|
|
|
|
// Runner runs one command, so the tools can be tested without the machine.
|
|
type Runner func(ctx context.Context, name string, args ...string) Ran
|
|
|
|
// CallTimeout is how long one command may take: below the runtime's thirty-second call limit, so a
|
|
// command that hangs is answered as such rather than as a call the runtime gave up on.
|
|
const CallTimeout = 20 * time.Second
|
|
|
|
// outputLimit bounds what one command may hand back, so a runaway listing cannot exhaust the
|
|
// process; well above anything a tool answers.
|
|
const outputLimit = 16 << 20
|
|
|
|
type bounded struct {
|
|
bytes.Buffer
|
|
cut bool
|
|
}
|
|
|
|
func (b *bounded) Write(p []byte) (int, error) {
|
|
if room := outputLimit - b.Len(); room < len(p) {
|
|
if room > 0 {
|
|
b.Buffer.Write(p[:room])
|
|
}
|
|
b.cut = true
|
|
return len(p), nil
|
|
}
|
|
return b.Buffer.Write(p)
|
|
}
|
|
|
|
// ExecRunner runs a command on this machine, in the C locale so what is parsed is one language.
|
|
func ExecRunner(ctx context.Context, name string, args ...string) Ran {
|
|
return execWithin(ctx, CallTimeout, nil, name, args...)
|
|
}
|
|
|
|
// execWithin runs a command on this machine within timeout, in the C locale, with env added to this process's.
|
|
func execWithin(ctx context.Context, timeout time.Duration, env []string, name string, args ...string) Ran {
|
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, name, args...)
|
|
cmd.Env = append(append(os.Environ(), "LC_ALL=C"), env...)
|
|
var out, errb bounded
|
|
cmd.Stdout, cmd.Stderr = &out, &errb
|
|
err := cmd.Run()
|
|
r := Ran{Stdout: out.String(), Stderr: errb.String()}
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
r.Status, r.Err = 124, fmt.Sprintf("no answer within %d s", int(timeout.Seconds()))
|
|
return r
|
|
}
|
|
var exit *exec.ExitError
|
|
switch {
|
|
case err == nil:
|
|
case errors.As(err, &exit):
|
|
r.Status = exit.ExitCode()
|
|
case errors.Is(err, exec.ErrNotFound) || errors.Is(err, fs.ErrNotExist):
|
|
r.Status, r.Err = 127, "ENOENT"
|
|
default:
|
|
r.Status, r.Err = 126, err.Error()
|
|
}
|
|
return r
|
|
}
|
|
|
|
// Escalated is the command as it is run: as given when this process is root, else through sudo
|
|
// without a prompt.
|
|
func Escalated(uid int, name string, args ...string) (string, []string) {
|
|
if uid == 0 {
|
|
return name, args
|
|
}
|
|
return "sudo", append([]string{"-n", name}, args...)
|
|
}
|
|
|
|
// Machine is this machine as the tools see it: a runner, who this process is, and its files.
|
|
type Machine struct {
|
|
Run Runner
|
|
UID int
|
|
User string
|
|
Account string
|
|
ReadFile func(path string) ([]byte, error)
|
|
Now func() time.Time
|
|
Sleep func(time.Duration)
|
|
// Node is the node this bundle runs on (the runtime's MESH_NODE), and States reads the module's states and
|
|
// those it reads (the manifest's `reads`), for the verbs that answer from the bus (novox/hq ADR 0507 §6).
|
|
Node string
|
|
States func(name string) StateReader
|
|
}
|
|
|
|
// StateReader is a state as a verb reads it: stdio.State, or a fake in a test.
|
|
type StateReader interface {
|
|
Get(key string) (*stdio.StateEntry, error)
|
|
Keys() ([]string, error)
|
|
History(key string) ([]stdio.StateRevision, error)
|
|
}
|
|
|
|
// ThisMachine is the machine the runtime launched this bundle on.
|
|
func ThisMachine() *Machine {
|
|
user := os.Getenv("USER")
|
|
if user == "" {
|
|
user = os.Getenv("LOGNAME")
|
|
}
|
|
account := strings.TrimSpace(os.Getenv("MESH_OPERATOR_ACCOUNT"))
|
|
if account == "" {
|
|
account = user
|
|
}
|
|
return &Machine{Run: ExecRunner, UID: os.Getuid(), User: user, Account: account, ReadFile: os.ReadFile, Now: time.Now, Sleep: time.Sleep,
|
|
Node: os.Getenv("MESH_NODE"), States: func(name string) StateReader { return stdio.State(name) }}
|
|
}
|
|
|
|
// Out runs a command that only reads, and fails with what went wrong named.
|
|
func (m *Machine) Out(name string, args ...string) (string, error) {
|
|
r := m.Run(context.Background(), name, args...)
|
|
if r.Status == 0 && r.Err == "" {
|
|
return r.Stdout, nil
|
|
}
|
|
return r.Stdout, failure(name, name, r)
|
|
}
|
|
|
|
// Root runs a command that needs root, escalated when this process is not.
|
|
func (m *Machine) Root(name string, args ...string) (string, error) {
|
|
program, argv := Escalated(m.UID, name, args...)
|
|
r := m.Run(context.Background(), program, argv...)
|
|
if r.Status == 0 && r.Err == "" {
|
|
return r.Stdout, nil
|
|
}
|
|
return r.Stdout, failure(name, program, r)
|
|
}
|
|
|
|
// RootRan is Root's raw answer, for a command whose non-zero status is itself an answer.
|
|
func (m *Machine) RootRan(name string, args ...string) (Ran, error) {
|
|
program, argv := Escalated(m.UID, name, args...)
|
|
r := m.Run(context.Background(), program, argv...)
|
|
if r.Err != "" || (program == "sudo" && sudoRefused(r)) {
|
|
return r, failure(name, program, r)
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func sudoRefused(r Ran) bool {
|
|
return strings.HasPrefix(strings.TrimSpace(r.Stderr), "sudo:")
|
|
}
|
|
|
|
// failure names what failed by how it failed: the program missing is a spawn error, sudo missing
|
|
// or refusing speaks for itself, and the rest is the command's own first line.
|
|
func failure(cmd, program string, r Ran) error {
|
|
said := strings.TrimSpace(r.Stderr + "\n" + r.Stdout)
|
|
if r.Err == "ENOENT" {
|
|
if program == "sudo" {
|
|
return fmt.Errorf("%s needs root for this, and sudo is not installed here for the runtime's account to escalate with", cmd)
|
|
}
|
|
return fmt.Errorf("%s is not installed on this machine", cmd)
|
|
}
|
|
if r.Err != "" {
|
|
return fmt.Errorf("%s did not answer: %s", cmd, r.Err)
|
|
}
|
|
if program == "sudo" && sudoRefused(r) {
|
|
if strings.Contains(said, "command not found") {
|
|
return fmt.Errorf("%s is not installed on this machine", cmd)
|
|
}
|
|
return fmt.Errorf("%s needs root for this and the runtime's account may not run it without a prompt: %s", cmd, firstLine(said))
|
|
}
|
|
if line := firstLine(said); line != "" {
|
|
return fmt.Errorf("%s failed (%d): %s", cmd, r.Status, line)
|
|
}
|
|
return fmt.Errorf("%s failed with status %d", cmd, r.Status)
|
|
}
|
|
|
|
func firstLine(text string) string {
|
|
for _, l := range strings.Split(text, "\n") {
|
|
if l = strings.TrimSpace(l); l != "" {
|
|
return l
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func lines(text string) []string {
|
|
var out []string
|
|
for _, l := range strings.Split(text, "\n") {
|
|
if l = strings.TrimRight(l, "\r"); strings.TrimSpace(l) != "" {
|
|
out = append(out, l)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// text is a string argument; required says whether it may be absent. It is never something a
|
|
// command would read as an option, which under sudo would be root's option.
|
|
func text(args map[string]any, key string, required bool) (string, error) {
|
|
raw, present := args[key]
|
|
if !present || raw == nil {
|
|
if required {
|
|
return "", fmt.Errorf("%s is required", key)
|
|
}
|
|
return "", nil
|
|
}
|
|
s, ok := raw.(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("%s must be a string", key)
|
|
}
|
|
s = strings.TrimSpace(s)
|
|
if required && s == "" {
|
|
return "", fmt.Errorf("%s is required", key)
|
|
}
|
|
if strings.HasPrefix(s, "-") || strings.ContainsRune(s, 0) || strings.ContainsAny(s, "\n\r") {
|
|
return "", fmt.Errorf("%s %q is not a value this tool passes on", key, s)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// whole is a whole-number argument with a default, kept within bounds.
|
|
func whole(args map[string]any, key string, def, least, most int) (int, error) {
|
|
raw, present := args[key]
|
|
if !present || raw == nil {
|
|
return def, nil
|
|
}
|
|
f, ok := raw.(float64)
|
|
if !ok || f != float64(int(f)) {
|
|
return 0, fmt.Errorf("%s must be a whole number", key)
|
|
}
|
|
n := int(f)
|
|
if n < least {
|
|
return 0, fmt.Errorf("%s must be at least %d", key, least)
|
|
}
|
|
if n > most {
|
|
n = most
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// flag is a boolean argument, false when absent.
|
|
func flag(args map[string]any, key string) (bool, error) {
|
|
raw, present := args[key]
|
|
if !present || raw == nil {
|
|
return false, nil
|
|
}
|
|
b, ok := raw.(bool)
|
|
if !ok {
|
|
return false, fmt.Errorf("%s must be true or false", key)
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// schema is a tool's input: its properties and the ones it requires.
|
|
func schema(properties map[string]any, required ...string) map[string]any {
|
|
s := map[string]any{"type": "object", "properties": properties}
|
|
if len(required) > 0 {
|
|
s["required"] = required
|
|
}
|
|
return s
|
|
}
|
|
|
|
// unitProps reads a unit's properties as systemctl shows them.
|
|
func (m *Machine) unitProps(unit string, props ...string) (map[string]string, error) {
|
|
args := []string{"show", unit, "--no-pager"}
|
|
for _, p := range props {
|
|
args = append(args, "--property="+p)
|
|
}
|
|
out, err := m.Out("systemctl", args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return keyValues(out, "="), nil
|
|
}
|
|
|
|
// keyValues reads `key<sep>value` lines; a line without the separator is skipped.
|
|
func keyValues(out, sep string) map[string]string {
|
|
kv := map[string]string{}
|
|
for _, l := range strings.Split(out, "\n") {
|
|
k, v, ok := strings.Cut(l, sep)
|
|
if ok {
|
|
kv[strings.TrimSpace(k)] = strings.TrimSpace(v)
|
|
}
|
|
}
|
|
return kv
|
|
}
|