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
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).
116 lines
4.4 KiB
Go
116 lines
4.4 KiB
Go
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)
|
||
}
|