~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
818 lines · 23.1 KB Raw History
//go:build linux

package hostsvc

import (
	"cmp"
	"context"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"net"
	"slices"
	"sort"
	"strings"
	"sync"
	"syscall"
	"time"

	sd "github.com/coreos/go-systemd/v22/dbus"
	"github.com/godbus/dbus/v5"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/host"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/strs"
)

const (
	errUnitExists           = "org.freedesktop.systemd1.UnitExists"
	errNoSuchUnit           = "org.freedesktop.systemd1.NoSuchUnit"
	errNoUnitForPID         = "org.freedesktop.systemd1.NoUnitForPID"
	errNoSuchProcess        = "org.freedesktop.systemd1.NoSuchProcess"
	errUnixProcessIDUnknown = "org.freedesktop.DBus.Error.UnixProcessIdUnknown"
)

var _ host.Units = (*systemd)(nil)

type systemd struct {
	dial    func(ctx context.Context) (bus, error)
	procs   procs
	timeout time.Duration
	prefix  string

	mu sync.Mutex
	b  bus
}

func openSystemd(ctx context.Context, cfg Config) (Manager, error) {
	path := cfg.SystemdPrivateSocket
	if path == "" {
		path = PrivateSocketPath()
	}
	m, err := OpenPrivate(ctx, path)
	if err != nil {
		return nil, err
	}
	m.(*systemd).prefix = cfg.Prefix()
	return m, nil
}

func OpenPrivate(ctx context.Context, path string) (Manager, error) {
	m := newManager(dialPrivate(path), linuxProcs{}, DefaultCallTimeout)
	if err := m.checkVersion(ctx); err != nil {
		m.Close()
		return nil, err
	}
	return m, nil
}

func newManager(dial func(ctx context.Context) (bus, error), p procs, timeout time.Duration) *systemd {
	return &systemd{dial: dial, procs: p, timeout: timeout, prefix: NamePrefix}
}

func (m *systemd) Capabilities() Capabilities {
	return Capabilities{Backend: BackendSystemdUser, EnforcesLimits: true, OOMKillReported: true, FDStore: true}
}

func (m *systemd) owns(name string) error {
	if !strings.HasPrefix(name, m.prefix) {
		return fmt.Errorf("hostsvc: unit %s is outside this manager's prefix %s", name, m.prefix)
	}
	return nil
}

func (m *systemd) managerID(ctx context.Context) (string, error) {
	var started uint64
	err := m.do(ctx, func(ctx context.Context, b bus) (err error) {
		started, err = b.ManagerStarted(ctx)
		return err
	})
	if err != nil {
		return "", err
	}
	boot, err := m.procs.BootID()
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("systemd-user:%s:%d", boot, started), nil
}

func (m *systemd) StopSlice(ctx context.Context, slice string) error {
	if err := ValidateSlice(slice); err != nil {
		return err
	}
	ctx, cancel := withDeadline(ctx, m.timeout)
	defer cancel()
	ch := make(chan string, 1)
	err := m.do(ctx, func(ctx context.Context, b bus) error { return b.StopUnit(ctx, slice, ch) })
	if dbusErrorName(err) == errNoSuchUnit {
		return nil
	}
	if err != nil {
		return fmt.Errorf("hostsvc: stopping %s: %w", slice, err)
	}
	result, err := waitJob(ctx, ch)
	if err != nil {
		return fmt.Errorf("hostsvc: stopping %s: %w", slice, err)
	}
	if result != "done" {
		return fmt.Errorf("hostsvc: the stop job of %s ended %q", slice, result)
	}
	return nil
}

const unitPathPrefix = "/org/freedesktop/systemd1/unit/"

func unitKey(st UnitStatus) string {
	return string(st.State) + "|" + st.SubState + "|" + st.InvocationID + "|" + st.Result
}

func unescapeUnitPath(p dbus.ObjectPath) string {
	s, ok := strings.CutPrefix(string(p), unitPathPrefix)
	if !ok || strings.Contains(s, "/") {
		return ""
	}
	var out []byte
	for i := 0; i < len(s); i++ {
		if s[i] == '_' && i+2 < len(s) {
			if b, err := hex.DecodeString(s[i+1 : i+3]); err == nil {
				out = append(out, b[0])
				i += 2
				continue
			}
		}
		out = append(out, s[i])
	}
	return string(out)
}

func signalUnit(sig *dbus.Signal) string {
	switch sig.Name {
	case managerIfc + ".UnitNew", managerIfc + ".UnitRemoved":
		if len(sig.Body) > 0 {
			name, _ := sig.Body[0].(string)
			return name
		}
	case managerIfc + ".JobRemoved":
		if len(sig.Body) > 2 {
			name, _ := sig.Body[2].(string)
			return name
		}
	case "org.freedesktop.DBus.Properties.PropertiesChanged":
		if len(sig.Body) > 0 {
			if ifc, _ := sig.Body[0].(string); ifc == systemdDest+".Unit" || ifc == systemdDest+".Service" {
				return unescapeUnitPath(sig.Path)
			}
		}
	}
	return ""
}

func (m *systemd) current(ctx context.Context, name string) (UnitStatus, error) {
	var units []sd.UnitStatus
	err := m.do(ctx, func(ctx context.Context, b bus) (err error) {
		units, err = b.ListUnitsByPatterns(ctx, []string{name})
		return err
	})
	if err != nil {
		return UnitStatus{}, fmt.Errorf("hostsvc: looking up %s: %w", name, err)
	}
	for _, u := range units {
		if u.Name == name && u.LoadState != "not-found" {
			return m.Inspect(ctx, name)
		}
	}
	return UnitStatus{Name: name, State: NotFound}, nil
}

func (m *systemd) Watch(ctx context.Context, prefix string) (<-chan UnitEvent, error) {
	if err := ValidatePrefix(prefix); err != nil {
		return nil, err
	}
	dctx, cancel := withDeadline(ctx, m.timeout)
	b, err := m.dial(dctx)
	cancel()
	if err != nil {
		return nil, fmt.Errorf("%w: %w", ErrUnreachable, err)
	}
	sigs := make(chan *dbus.Signal, 256)
	sctx, cancel := withDeadline(ctx, m.timeout)
	err = b.Subscribe(sctx, sigs)
	cancel()
	if err != nil {
		b.Close()
		if unreachable(err) {
			return nil, fmt.Errorf("%w: subscribing to unit signals: %w", ErrUnreachable, err)
		}
		return nil, fmt.Errorf("hostsvc: subscribing to unit signals: %w", err)
	}
	first, err := m.List(ctx, prefix)
	if err != nil {
		b.Close()
		return nil, err
	}
	seen := map[string]string{}
	for _, st := range first {
		seen[st.Name] = unitKey(st)
	}
	ch := make(chan UnitEvent, 64)
	stop := context.AfterFunc(ctx, b.Close)
	go func() {
		defer close(ch)
		defer stop()
		defer b.Close()
		for sig := range sigs {
			name := signalUnit(sig)
			if !strings.HasPrefix(name, prefix) || ValidateName(name) != nil {
				continue
			}
			st, err := m.current(ctx, name)
			if errors.Is(err, ErrUnreachable) || ctx.Err() != nil {
				return
			}
			if err != nil {
				continue
			}
			if st.State == NotFound {
				if _, ok := seen[name]; !ok {
					continue
				}
				delete(seen, name)
			} else {
				k := unitKey(st)
				if seen[name] == k {
					continue
				}
				seen[name] = k
			}
			select {
			case ch <- UnitEvent{Name: name, Status: st}:
			case <-ctx.Done():
				return
			}
		}
	}()
	return ch, nil
}

func withDeadline(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) {
	if _, ok := ctx.Deadline(); ok {
		return context.WithCancel(ctx)
	}
	return context.WithTimeout(ctx, d)
}

func (m *systemd) conn(ctx context.Context) (bus, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.b != nil {
		return m.b, nil
	}
	b, err := m.dial(ctx)
	if err != nil {
		return nil, fmt.Errorf("%w: %w", ErrUnreachable, err)
	}
	m.b = b
	return b, nil
}

func (m *systemd) drop(b bus) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.b == b {
		m.b = nil
	}
	b.Close()
}

func (m *systemd) do(ctx context.Context, f func(ctx context.Context, b bus) error) error {
	ctx, cancel := withDeadline(ctx, m.timeout)
	defer cancel()
	for retried := false; ; retried = true {
		b, err := m.conn(ctx)
		if err != nil {
			return err
		}
		err = f(ctx, b)
		if err == nil || !unreachable(err) {
			return err
		}
		if ctx.Err() != nil {
			return fmt.Errorf("%w: %w", ErrUnreachable, err)
		}
		m.drop(b)
		if retried {
			return fmt.Errorf("%w: %w", ErrUnreachable, err)
		}
	}
}

func unreachable(err error) bool {
	if err == nil {
		return false
	}
	var de dbus.Error
	if errors.As(err, &de) {
		return false
	}
	var dl dialError
	if errors.As(err, &dl) {
		return true
	}
	for _, target := range []error{context.DeadlineExceeded, context.Canceled, dbus.ErrClosed, net.ErrClosed,
		io.EOF, io.ErrUnexpectedEOF, syscall.ECONNRESET, syscall.EPIPE, syscall.ENOTCONN, syscall.ECONNREFUSED} {
		if errors.Is(err, target) {
			return true
		}
	}
	var op *net.OpError
	return errors.As(err, &op)
}

func dbusErrorName(err error) string {
	var de dbus.Error
	if errors.As(err, &de) {
		return de.Name
	}
	return ""
}

type envFile struct {
	Path          string
	IgnoreMissing bool
}

func unitProperties(spec UnitSpec) []sd.Property {
	desc := spec.Description
	desc = cmp.Or(desc, spec.Name)
	slice := spec.Slice
	slice = cmp.Or(slice, DefaultSlice)
	oom := spec.OOMPolicy
	oom = cmp.Or(oom, "stop")
	str := func(name, v string) sd.Property { return sd.Property{Name: name, Value: dbus.MakeVariant(v)} }
	u64 := func(name string, v uint64) sd.Property { return sd.Property{Name: name, Value: dbus.MakeVariant(v)} }
	props := []sd.Property{
		sd.PropDescription(desc),
		sd.PropExecStart(spec.Exec, false),
		str("Type", "exec"),
		str("CollectMode", "inactive"),
		str("KillMode", "control-group"),
		str("OOMPolicy", oom),
		sd.PropSlice(slice),
	}
	restart := spec.Restart
	restart = cmp.Or(restart, RestartNo)
	props = append(props, str("Restart", string(restart)))
	if spec.KillSignal != 0 {
		props = append(props, sd.Property{Name: "KillSignal", Value: dbus.MakeVariant(int32(spec.KillSignal))})
	}
	if spec.StopTimeout > 0 {
		props = append(props, u64("TimeoutStopUSec", uint64(spec.StopTimeout/time.Microsecond)))
	}
	if spec.Limits.MemoryMax > 0 {
		props = append(props, u64("MemoryMax", spec.Limits.MemoryMax), u64("MemorySwapMax", 0))
	}
	if spec.Delegate {
		props = append(props, sd.Property{Name: "Delegate", Value: dbus.MakeVariant(true)})
	}
	if spec.Limits.TasksMax > 0 {
		props = append(props, u64("TasksMax", spec.Limits.TasksMax))
	}
	if spec.Limits.CPUWeight > 0 {
		props = append(props, u64("CPUWeight", spec.Limits.CPUWeight))
	}
	if spec.Limits.CPUQuotaPercent > 0 {
		props = append(props, u64("CPUQuotaPerSecUSec", uint64(spec.Limits.CPUQuotaPercent)*10_000))
	}
	if len(spec.Env) > 0 {
		props = append(props, sd.Property{Name: "Environment", Value: dbus.MakeVariant(spec.Env)})
	}
	if len(spec.EnvFiles) > 0 {
		files := make([]envFile, 0, len(spec.EnvFiles))
		for _, f := range spec.EnvFiles {
			files = append(files, envFile{Path: f})
		}
		props = append(props, sd.Property{Name: "EnvironmentFiles", Value: dbus.MakeVariant(files)})
	}
	if spec.WorkDir != "" {
		props = append(props, str("WorkingDirectory", spec.WorkDir))
	}
	if spec.Output.StdoutFile != "" {
		props = append(props, str("StandardOutputFileToAppend", spec.Output.StdoutFile))
	}
	if spec.Output.StderrFile != "" {
		props = append(props, str("StandardErrorFileToAppend", spec.Output.StderrFile))
	}
	if len(spec.After) > 0 {
		props = append(props, sd.PropAfter(spec.After...))
	}
	if len(spec.Wants) > 0 {
		props = append(props, sd.PropWants(spec.Wants...))
	}
	if spec.FDStore > 0 {
		props = append(props, sd.Property{Name: "FileDescriptorStoreMax", Value: dbus.MakeVariant(uint32(spec.FDStore))},
			str("NotifyAccess", "main"))
	}
	return props
}

func (m *systemd) checkVersion(ctx context.Context) error {
	var v string
	err := m.do(ctx, func(ctx context.Context, b bus) (err error) {
		v, err = b.Version(ctx)
		return err
	})
	if err != nil {
		return fmt.Errorf("hostsvc: reading the manager version: %w", err)
	}
	n, err := strs.LeadingInt(v)
	if err != nil {
		return fmt.Errorf("%w: the manager reports version %q", ErrSystemdTooOld, v)
	}
	if n < MinSystemdVersion {
		return fmt.Errorf("%w: the manager reports %q", ErrSystemdTooOld, v)
	}
	return nil
}

func waitJob(ctx context.Context, ch <-chan string) (string, error) {
	select {
	case r := <-ch:
		return r, nil
	case <-ctx.Done():
		return "", fmt.Errorf("%w: waiting for the job: %w", ErrUnreachable, ctx.Err())
	}
}

func (m *systemd) Start(ctx context.Context, spec UnitSpec) (UnitStatus, error) {
	if err := ValidateSpec(spec); err != nil {
		return UnitStatus{}, err
	}
	if err := m.owns(spec.Name); err != nil {
		return UnitStatus{}, err
	}
	if err := RefuseUnsupported(m.Capabilities(), spec); err != nil {
		return UnitStatus{}, err
	}
	ctx, cancel := withDeadline(ctx, m.timeout)
	defer cancel()
	props := unitProperties(spec)
	ch := make(chan string, 1)
	err := m.do(ctx, func(ctx context.Context, b bus) error {
		return b.StartTransientUnit(ctx, spec.Name, props, ch)
	})
	if dbusErrorName(err) == errUnitExists {
		st, err := m.Inspect(ctx, spec.Name)
		st.Adopted = true
		return st, err
	}
	if err != nil {
		return UnitStatus{}, fmt.Errorf("hostsvc: starting %s: %w", spec.Name, err)
	}
	result, err := waitJob(ctx, ch)
	if err != nil {
		return UnitStatus{}, fmt.Errorf("hostsvc: starting %s: %w", spec.Name, err)
	}
	st, err := m.Inspect(ctx, spec.Name)
	if err != nil {
		return st, err
	}
	if result != "done" {
		return st, fmt.Errorf("hostsvc: the start job of %s ended %q (state %s, result %s, exec main code %d status %d)",
			spec.Name, result, st.State, st.Result, st.ExecMainCode, st.ExecMainStatus)
	}
	return st, nil
}

type unitStarter interface {
	StartUnit(ctx context.Context, name string, ch chan<- string) error
}

type bootFiles interface {
	EnableUnitFiles(ctx context.Context, names []string) error
	DisableUnitFiles(ctx context.Context, names []string) error
}

func (m *systemd) SetBoot(ctx context.Context, enable, disable []string) error {
	for _, n := range append(slices.Clone(enable), disable...) {
		if err := ValidateName(n); err != nil {
			return err
		}
		if err := m.owns(n); err != nil {
			return err
		}
	}
	ctx, cancel := withDeadline(ctx, m.timeout)
	defer cancel()
	return m.do(ctx, func(ctx context.Context, b bus) error {
		s, ok := b.(bootFiles)
		if !ok {
			return fmt.Errorf("%w: this bus enables no unit file", ErrUnsupported)
		}
		if len(disable) > 0 {
			if err := s.DisableUnitFiles(ctx, disable); err != nil {
				return fmt.Errorf("hostsvc: disabling %v: %w", disable, err)
			}
		}
		if len(enable) > 0 {
			if err := s.EnableUnitFiles(ctx, enable); err != nil {
				return fmt.Errorf("hostsvc: enabling %v: %w", enable, err)
			}
		}
		return nil
	})
}

func (m *systemd) StartInstalled(ctx context.Context, name string) (UnitStatus, error) {
	if err := ValidateName(name); err != nil {
		return UnitStatus{}, err
	}
	if err := m.owns(name); err != nil {
		return UnitStatus{}, err
	}
	ctx, cancel := withDeadline(ctx, m.timeout)
	defer cancel()
	if st, err := m.Inspect(ctx, name); err == nil && st.State == Active {
		st.Adopted = true
		return st, nil
	}
	ch := make(chan string, 1)
	err := m.do(ctx, func(ctx context.Context, b bus) error {
		s, ok := b.(unitStarter)
		if !ok {
			return fmt.Errorf("%w: this bus starts no installed unit", ErrUnsupported)
		}
		return s.StartUnit(ctx, name, ch)
	})
	if err != nil {
		return UnitStatus{}, fmt.Errorf("hostsvc: starting the installed unit %s: %w", name, err)
	}
	result, err := waitJob(ctx, ch)
	if err != nil {
		return UnitStatus{}, fmt.Errorf("hostsvc: starting the installed unit %s: %w", name, err)
	}
	st, err := m.Inspect(ctx, name)
	if err != nil {
		return st, err
	}
	if result != "done" {
		return st, fmt.Errorf("hostsvc: the start job of the installed unit %s ended %q (state %s, result %s)", name, result, st.State, st.Result)
	}
	return st, nil
}

func (m *systemd) Inspect(ctx context.Context, name string) (UnitStatus, error) {
	if err := ValidateName(name); err != nil {
		return UnitStatus{}, err
	}
	var up, sp map[string]any
	err := m.do(ctx, func(ctx context.Context, b bus) (err error) {
		if up, err = b.UnitProperties(ctx, name); err != nil {
			return err
		}
		sp, err = b.ServiceProperties(ctx, name)
		return err
	})
	if err != nil {
		return UnitStatus{}, fmt.Errorf("hostsvc: inspecting %s: %w", name, err)
	}
	return statusFrom(name, up, sp), nil
}

func statusFrom(name string, up, sp map[string]any) UnitStatus {
	st := UnitStatus{Name: name, Backend: BackendSystemdUser}
	str := func(m map[string]any, k string) string { s, _ := m[k].(string); return s }
	if str(up, "LoadState") == "not-found" {
		st.State = NotFound
		return st
	}
	st.State = ActiveState(str(up, "ActiveState"))
	st.SubState = str(up, "SubState")
	st.Description = str(up, "Description")
	st.Result = str(sp, "Result")
	if inv, ok := up["InvocationID"].([]byte); ok {
		st.InvocationID = hex.EncodeToString(inv)
	}
	if ts, ok := up["ActiveEnterTimestamp"].(uint64); ok && ts > 0 {
		st.ActiveSince = time.UnixMicro(int64(ts)).UTC()
	}
	if pid, ok := sp["MainPID"].(uint32); ok {
		st.MainPID = int(pid)
	}
	st.ExecMainCode, _ = sp["ExecMainCode"].(int32)
	st.ExecMainStatus, _ = sp["ExecMainStatus"].(int32)
	return st
}

func (m *systemd) Stop(ctx context.Context, name string) error {
	if err := ValidateName(name); err != nil {
		return err
	}
	ctx, cancel := withDeadline(ctx, m.timeout)
	defer cancel()
	ch := make(chan string, 1)
	err := m.do(ctx, func(ctx context.Context, b bus) error { return b.StopUnit(ctx, name, ch) })
	if dbusErrorName(err) == errNoSuchUnit {
		return nil
	}
	if err != nil {
		return fmt.Errorf("hostsvc: stopping %s: %w", name, err)
	}
	result, err := waitJob(ctx, ch)
	if err != nil {
		return fmt.Errorf("hostsvc: stopping %s: %w", name, err)
	}
	if result != "done" {
		return fmt.Errorf("hostsvc: the stop job of %s ended %q", name, result)
	}
	return nil
}

func (m *systemd) ResetFailed(ctx context.Context, name string) error {
	if err := ValidateName(name); err != nil {
		return err
	}
	err := m.do(ctx, func(ctx context.Context, b bus) error { return b.ResetFailedUnit(ctx, name) })
	if dbusErrorName(err) == errNoSuchUnit {
		return nil
	}
	if err != nil {
		return fmt.Errorf("hostsvc: resetting %s: %w", name, err)
	}
	return nil
}

func (m *systemd) List(ctx context.Context, prefix string) ([]UnitStatus, error) {
	if err := ValidatePrefix(prefix); err != nil {
		return nil, err
	}
	var units []sd.UnitStatus
	err := m.do(ctx, func(ctx context.Context, b bus) (err error) {
		units, err = b.ListUnitsByPatterns(ctx, []string{prefix + "*"})
		return err
	})
	if err != nil {
		return nil, fmt.Errorf("hostsvc: listing %s*: %w", prefix, err)
	}
	var out []UnitStatus
	for _, u := range units {
		if !strings.HasPrefix(u.Name, prefix) || !strings.HasSuffix(u.Name, ServiceSuffix) || u.LoadState == "not-found" {
			continue
		}
		st, err := m.Inspect(ctx, u.Name)
		if err != nil {
			return nil, err
		}
		if st.State != NotFound {
			out = append(out, st)
		}
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
	return out, nil
}

func (m *systemd) unitByPIDFD(ctx context.Context, fd int) (string, string, error) {
	var unit, inv string
	err := m.do(ctx, func(ctx context.Context, b bus) (err error) {
		unit, inv, err = b.UnitByPIDFD(ctx, fd)
		return err
	})
	return unit, inv, err
}

func (m *systemd) Identify(ctx context.Context, name string, pid int) (Identity, error) {
	st, err := m.Inspect(ctx, name)
	if err != nil {
		return Identity{}, err
	}
	if st.State != Active {
		return Identity{}, fmt.Errorf("hostsvc: %s is %s, not active, and has no identity to record", name, st.State)
	}
	pid = cmp.Or(pid, st.MainPID)
	boot, err := m.procs.BootID()
	if err != nil {
		return Identity{}, err
	}
	p, err := m.procs.Pin(pid)
	if err != nil {
		return Identity{}, fmt.Errorf("hostsvc: pinning pid %d of %s: %w", pid, name, err)
	}
	defer p.Close()
	unit, inv, err := m.unitByPIDFD(ctx, p.FD())
	if err != nil {
		return Identity{}, fmt.Errorf("hostsvc: GetUnitByPIDFD for pid %d of %s: %w", pid, name, err)
	}
	if unit != name || inv != st.InvocationID {
		return Identity{}, fmt.Errorf("hostsvc: pid %d belongs to %s invocation %s, not %s invocation %s",
			pid, unit, inv, name, st.InvocationID)
	}
	mgr, err := m.managerID(ctx)
	if err != nil {
		return Identity{}, fmt.Errorf("hostsvc: reading the manager identity: %w", err)
	}
	return Identity{Unit: name, InvocationID: inv, PID: pid, StartTicks: p.StartTicks(), BootID: boot,
		Backend: BackendSystemdUser, Manager: mgr}, nil
}

func (m *systemd) Check(ctx context.Context, want Identity) (Observation, error) {
	if err := ValidateName(want.Unit); err != nil {
		return Observation{}, err
	}
	boot, err := m.procs.BootID()
	if err != nil {
		return Observation{Detail: err.Error()}, err
	}
	if boot != want.BootID {
		return Observation{Verdict: VerdictLost, Reason: LostHostRebooted,
			Detail: fmt.Sprintf("boot id %s, recorded %s", boot, want.BootID)}, nil
	}
	if obs, foreign := ForeignIdentity(m.Capabilities(), want); foreign {
		return obs, nil
	}
	if want.Manager != "" {
		mgr, err := m.managerID(ctx)
		if errors.Is(err, ErrUnreachable) {
			return Observation{Verdict: VerdictUnknown, Detail: err.Error()}, nil
		}
		if err != nil {
			return Observation{Detail: err.Error()}, err
		}
		if mgr != want.Manager {
			return Observation{Verdict: VerdictLost, Reason: LostUnitGone,
				Detail: fmt.Sprintf("the user manager restarted (%s, recorded %s): every unit it held is gone", mgr, want.Manager)}, nil
		}
	}
	st, err := m.Inspect(ctx, want.Unit)
	if errors.Is(err, ErrUnreachable) {
		return Observation{Verdict: VerdictUnknown, Detail: err.Error()}, nil
	}
	if err != nil {
		return Observation{Detail: err.Error()}, err
	}
	lost := func(r LostReason, format string, args ...any) (Observation, error) {
		return Observation{Verdict: VerdictLost, Reason: r, Detail: fmt.Sprintf(format, args...), Status: st}, nil
	}
	switch st.State {
	case NotFound:
		return lost(LostUnitGone, "%s is not loaded", want.Unit)
	case Failed:
		if st.Result == "oom-kill" {
			return lost(LostOOMKilled, "%s failed: result %s", want.Unit, st.Result)
		}
		return lost(LostUnitFailed, "%s failed: result %s, exec main code %d status %d",
			want.Unit, st.Result, st.ExecMainCode, st.ExecMainStatus)
	case Inactive:
		return lost(LostUnitExited, "%s is inactive: result %s", want.Unit, st.Result)
	}
	if st.InvocationID != want.InvocationID {
		return m.mismatch(ctx, st, "%s runs invocation %s, recorded %s", want.Unit, st.InvocationID, want.InvocationID)
	}
	p, err := m.procs.Pin(want.PID)
	if errors.Is(err, errProcessGone) {
		return m.mismatch(ctx, st, "recorded pid %d of %s: %v", want.PID, want.Unit, err)
	}
	if err != nil {
		return Observation{Detail: err.Error(), Status: st}, err
	}
	defer p.Close()
	if p.StartTicks() != want.StartTicks {
		return m.mismatch(ctx, st, "pid %d started at tick %d, recorded %d: the pid was reused",
			want.PID, p.StartTicks(), want.StartTicks)
	}
	unit, inv, err := m.unitByPIDFD(ctx, p.FD())
	switch {
	case errors.Is(err, ErrUnreachable):
		return Observation{Verdict: VerdictUnknown, Detail: err.Error(), Status: st}, nil
	case dbusErrorName(err) == errUnixProcessIDUnknown || dbusErrorName(err) == errNoUnitForPID ||
		dbusErrorName(err) == errNoSuchProcess:
		return m.mismatch(ctx, st, "GetUnitByPIDFD for pid %d: %v", want.PID, err)
	case err != nil:
		return Observation{Detail: err.Error(), Status: st}, fmt.Errorf("hostsvc: GetUnitByPIDFD for pid %d: %w", want.PID, err)
	}
	if unit != want.Unit || inv != want.InvocationID {
		return m.mismatch(ctx, st, "pid %d belongs to %s invocation %s, recorded %s invocation %s",
			want.PID, unit, inv, want.Unit, want.InvocationID)
	}
	if st.State != Active {
		return Observation{Verdict: VerdictUnknown, Detail: fmt.Sprintf("%s is %s", want.Unit, st.State), Status: st}, nil
	}
	return Observation{Verdict: VerdictRunning, Status: st}, nil
}

func (m *systemd) mismatch(ctx context.Context, st UnitStatus, format string, args ...any) (Observation, error) {
	obs := Observation{Verdict: VerdictLost, Reason: LostIdentityMismatch, Detail: fmt.Sprintf(format, args...), Status: st}
	if err := m.Stop(ctx, st.Name); err != nil {
		return obs, fmt.Errorf("hostsvc: %s does not match its record and stopping it failed: %w", st.Name, err)
	}
	obs.Stopped = true
	return obs, nil
}

func (m *systemd) Close() error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.b != nil {
		m.b.Close()
		m.b = nil
	}
	return nil
}