Files
mesh-host/internal/units/exec.go
T
jschoubben 62af89f85d
mesh/merge-gate head 62af89f85d0d merged into trunk 1a2677882380 — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head 62af89f85d0d merged into trunk 1a2677882380 — pass: its merge-check.sh passed
mesh/delivery delivered
Start no account's manager to look at its units (hq issue 764)
Every 15 s the failed-units look read the own manager of every account
the declaration creates, and reading one is a login that starts it: six
module accounts' managers on the control node started and stopped all
day, most of its journal. A look only reads (ADR 0240 rule 6) and never
asks an account's manager (ADR 0177).

An account's manager is read now only when the declaration places a
user-scoped unit in it, and only after the machine's own manager says
user@<uid>.service is active, as the apply asks before it. A manager not
running is said away in the units statement, with no failed units; in
liveness its units read unknown with the apply's words, never down. The
operator's account on a desktop is still read while logged in, since a
module places a user unit there (issue 315's Razer daemon stays said).
2026-10-15 00:33:34 +02:00

116 lines
4.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package units
import (
"context"
"errors"
"fmt"
"strings"
"github.com/novox/mesh-host/internal/system"
)
// Runner runs a command and answers what it printed — the apply's own (apply.ExecRunner), so a look reads
// the machine exactly as an apply does.
type Runner func(ctx context.Context, name string, args ...string) (string, error)
// Exec reads the service managers through systemctl and the package database through the system's own
// owner query. **Reads only**: `list-units`, `show`, the owner query, and — of the machine's manager and
// user database — whether an account's manager is active, are the whole of what it asks (ADR 0240 rule 6,
// and a test holds it).
type Exec struct {
Run Runner
// System is what the host was built for; it says how a file's package is asked. A system with no
// owner query answers "no package", so a unit there is the machine's unless the declaration states it
// or writes its file.
System string
}
// managerArgs is how systemctl is pointed at a manager: the machine's, or an account's own.
func managerArgs(scope, user string) []string {
if scope == ScopeUser && user != "" {
return []string{"--user", "--machine=" + user + "@"}
}
return nil
}
// Failed lists the failed units of one manager, in its plain form: one per line, the unit, its load, its
// active and sub state, and its description. A leading mark some versions print before a unit in trouble
// is skipped.
func (e Exec) Failed(ctx context.Context, scope, user string) ([]Listed, error) {
args := append(managerArgs(scope, user), "list-units", "--state=failed", "--all", "--plain", "--no-legend",
"--no-pager", "--full")
said, err := e.Run(ctx, "systemctl", args...)
if err != nil {
return nil, fmt.Errorf("the service manager could not be read: %w", err)
}
return parseFailed(said), nil
}
func parseFailed(said string) []Listed {
var out []Listed
for _, line := range strings.Split(said, "\n") {
fields := strings.Fields(line)
for len(fields) > 0 && (fields[0] == "●" || fields[0] == "*" || fields[0] == "×") {
fields = fields[1:]
}
if len(fields) < 2 || !strings.Contains(fields[0], ".") {
continue
}
out = append(out, Listed{Unit: fields[0], Load: fields[1]})
}
return out
}
// Show is one show of the units named: each one's file and how it failed.
func (e Exec) Show(ctx context.Context, scope, user string, units []string) (map[string]Shown, error) {
out := map[string]Shown{}
if len(units) == 0 {
return out, nil
}
args := append(managerArgs(scope, user), "show", "--property=Id,FragmentPath,Result", "--")
said, err := e.Run(ctx, "systemctl", append(args, units...)...)
if err != nil {
return nil, fmt.Errorf("the service manager could not be read: %w", err)
}
blocks := strings.Split(strings.TrimSpace(said), "\n\n")
if len(blocks) != len(units) {
return nil, errors.New("the service manager answered for a different number of units than were asked")
}
for i, block := range blocks {
props := map[string]string{}
for _, line := range strings.Split(block, "\n") {
if k, v, ok := strings.Cut(line, "="); ok {
props[strings.TrimSpace(k)] = strings.TrimSpace(v)
}
}
out[units[i]] = Shown{FragmentPath: props["FragmentPath"], Result: props["Result"]}
}
return out, nil
}
// PackageOwning asks the package database which package a file belongs to. pacman exits 1 both for a
// file no package owns and for a database it cannot read; it is asked about itself first, so the second
// is an error and only the first is "no package" (system/arch.go's two-step, for the same trap).
func (e Exec) PackageOwning(ctx context.Context, path string) (string, bool, error) {
switch e.System {
case "arch":
if _, err := e.Run(ctx, "pacman", "-Q", "pacman"); err != nil {
return "", false, fmt.Errorf("the package database does not answer: %w", err)
}
said, err := e.Run(ctx, "pacman", "-Qqo", path)
if err != nil {
return "", false, nil
}
pkg := strings.TrimSpace(firstLine(said))
return pkg, pkg != "", nil
}
return "", false, nil
}
// UserManagerRunning asks the machine's manager whether an account's own runs, as the apply does before it
// touches a user-scoped unit (system.UserManagerRunning; novox/hq ADR 0177, issue 764). A system with no
// manager per account cannot say, and is an error: that manager is said unread.
func (e Exec) UserManagerRunning(ctx context.Context, account string) (running, exists bool, err error) {
return system.ManagerRunning(ctx, e.System, system.Runner(e.Run), account)
}