Files
mesh-host/internal/units/exec.go
T
mesh-tools 5a7ec3775b
mesh/merge-gate head 5a7ec3775b98 merged into trunk 53fd32618916 beside mesh-controller#346@c711e622 — pass: builds mesh-host → ace, g14, novox, shanks; no…
mesh/repo-check head 5a7ec3775b98 merged into trunk 53fd32618916 beside mesh-controller#346@c711e622 — pass: [release feat/57-config-and-provisioning, judg…
mesh/delivery stopped: its walk plan-1792114347281764422 was stopped by a person (mesh-controller): mesh-host#114 merged into the release branch feat/57-…
mesh/delivery-group group 57-config-and-provisioning stopped: a member was stopped
Land feature 57's node-engine work on its release branch
The node-engine renders the feature's templates, collections and moved
providers' files, on the SDK the feature's modules build on (feature 57,
config and provisioning).
2026-10-15 21:13:28 +00: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)
}