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 }