Files
mesh-host/internal/units/exec.go
T
jochen 14e5d91c9a
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/315-a-failed-unit-is-a-condition delivered: every member is delivered
Say every failed unit, the module's and the machine's (hq issue 315)
Liveness judged only what a module runs long-lived, so a module whose
daemon is a package's unit started by D-Bus activation failed at every
start while its machine read healthy, and a degraded service manager was
said by nothing but the profile.

The engine now reads the failed units of the machine's manager and of
every account manager the declaration names, on the two-look rule, and
says whose each is: a declared service or process, a unit file the mesh
writes, or a package the mesh installs makes it that module's, said as
an unhealthy resource of kind unit; anything else is the machine's own,
said in the statement's new units field. It reads; it never acts.
2026-10-08 11:28:51 +02:00

106 lines
3.8 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"
)
// 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` and the owner query 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
}