~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
935 lines · 24.8 KB Raw History
package hostprocess

import (
	"cmp"
	"context"
	"fmt"
	"log/slog"
	"slices"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/watch"
	"k8s.io/client-go/rest"

	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/controllers/runtime/kcpapply"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/controllers/runtime/kube"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/controllers/runtime/manifests"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/hostsvc"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kcpserver"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/conds"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/fswatch"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/supervise"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/logcap"
)

const (
	ManifestSet      = "hostprocesses"
	ProviderSet      = "provider"
	DefaultWorkspace = "root"
	RetryAfter       = 2 * time.Second
	MaxRetryAfter    = time.Minute
	MirrorPrefix     = "bootstrap-"
	MirrorLabel      = "socialweb.computer/placement"
	ConditionReady   = "Ready"
	ConditionLimits  = "LimitsEnforced"
)

var GVR = v1alpha1.HostProcessGVR

type Mirror struct {
	Role  string
	Unit  string
	Exec  []string
	Slice string
	After []string
	Wants []string
	Port  int
}

type Config struct {
	Kubeconfig   string
	Workspace    string
	Manager      hostsvc.Manager
	Prefix       string
	Prefixes     []string
	Bound        bool
	Resolve      func(v1alpha1.HostProcessAsset) (string, error)
	Mirrors      func() []Mirror
	ApplyTimeout time.Duration
	Selector     string
	KeepOrphans  bool
	Name         string
	Log          *slog.Logger
}

type Supervisor struct {
	c       Config
	client  kube.Resource
	placed  map[string]bool
	listed  bool
	backend hostsvc.Backend

	mu       sync.Mutex
	locks    sync.Map
	placedAt map[string]time.Time
	keys     map[string]string
	rekeyed  map[string]string
	slices   map[string]bool
	units    map[string]string
	backoff  map[string]time.Duration
	pending  map[string]bool
	pids     map[string]int64
	outputs  map[string]follower
	follows  sync.WaitGroup
	files    *fswatch.Watcher
	changed  chan string
}

type follower struct {
	key  string
	stop context.CancelFunc
}

func (s *Supervisor) track(name, unit string, gone bool) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if gone {
		for u, n := range s.units {
			if n == name {
				delete(s.units, u)
			}
		}
		return
	}
	if unit == "" {
		return
	}
	if s.units == nil {
		s.units = map[string]string{}
	}
	s.units[unit] = name
}

func (s *Supervisor) objectOf(unit string) (string, bool) {
	s.mu.Lock()
	defer s.mu.Unlock()
	name, ok := s.units[unit]
	return name, ok
}

func (s *Supervisor) settle(ctx context.Context, name string, retry bool, pid int64) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if !retry {
		delete(s.backoff, name)
		delete(s.pids, name)
		return
	}
	if s.backoff == nil {
		s.backoff, s.pending, s.pids = map[string]time.Duration{}, map[string]bool{}, map[string]int64{}
	}
	if s.pids[name] != pid {
		delete(s.backoff, name)
		s.pids[name] = pid
	}
	if s.changed == nil || s.pending[name] {
		return
	}
	d := min(max(2*s.backoff[name], RetryAfter), MaxRetryAfter)
	s.backoff[name], s.pending[name] = d, true
	ch := s.changed
	time.AfterFunc(d, func() {
		s.mu.Lock()
		delete(s.pending, name)
		s.mu.Unlock()
		select {
		case ch <- name:
		case <-ctx.Done():
		}
	})
}

func (s *Supervisor) setPlaced(unit string, on bool) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if on {
		s.placed[unit] = true
		if s.placedAt == nil {
			s.placedAt = map[string]time.Time{}
		}
		s.placedAt[unit] = time.Now()
	} else {
		delete(s.placed, unit)
		delete(s.placedAt, unit)
		delete(s.keys, unit)
		delete(s.rekeyed, unit)
	}
}

func (s *Supervisor) setStartedKey(unit, key string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.keys == nil {
		s.keys = map[string]string{}
	}
	s.keys[unit] = key
}

func (s *Supervisor) setRekeyed(unit, key string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.rekeyed == nil {
		s.rekeyed = map[string]string{}
	}
	if key == "" {
		delete(s.rekeyed, unit)
		return
	}
	s.rekeyed[unit] = key
}

func (s *Supervisor) rekeyedFor(unit string) string {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.rekeyed[unit]
}

func (s *Supervisor) startedKey(unit string) (string, bool) {
	s.mu.Lock()
	defer s.mu.Unlock()
	key, ok := s.keys[unit]
	return key, ok
}

func (s *Supervisor) placedAfter(unit string, t time.Time) bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	at, ok := s.placedAt[unit]
	return ok && at.After(t)
}

func (s *Supervisor) isPlaced(unit string) bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.placed[unit]
}

func (s *Supervisor) lockObject(name string) func() {
	v, _ := s.locks.LoadOrStore(name, &sync.Mutex{})
	m := v.(*sync.Mutex)
	m.Lock()
	return m.Unlock
}

func (s *Supervisor) markAdopted(o v1alpha1.HostProcess, obs Observed) {
	if o.Spec.Placement == v1alpha1.HostProcessPlacementBootstrap || obs.Inspect != nil || s.isPlaced(o.Spec.Unit) {
		return
	}
	if obs.Unit.State == hostsvc.Active || obs.Unit.State == hostsvc.Activating {
		s.setPlaced(o.Spec.Unit, true)
	}
}

func (s *Supervisor) reconcileObject(ctx context.Context, o v1alpha1.HostProcess) (v1alpha1.HostProcess, bool) {
	unlock := s.lockObject(o.Name)
	defer unlock()
	cur := o.DeepCopy()
	s.track(o.Name, o.Spec.Unit, false)
	s.capOutput(o)
	obs := s.observe(ctx, o)
	s.markAdopted(o, obs)
	r := Reconcile(obs)
	next, failed := s.apply(ctx, o, r)
	err := s.writeStatus(ctx, cur, next)
	if err != nil && !apierrors.IsConflict(err) && !apierrors.IsNotFound(err) {
		s.c.log().Warn("writing status", "name", o.Name, "err", err.Error())
	}
	s.followOutput(ctx, next)
	rt := next.Status.Runtime
	listening := next.Status.Phase == v1alpha1.HostProcessPhaseRunning && rt != nil && rt.Pid > 0 && len(next.Status.Listeners) == 0
	var pid int64
	if listening {
		pid = rt.Pid
	}
	s.settle(ctx, o.Name, failed || r.Unknown || err != nil || listening, pid)
	return o, true
}

func (s *Supervisor) reconcileName(ctx context.Context, name string) {
	cur, err := kube.Get[v1alpha1.HostProcess](ctx, s.client, name)
	if err != nil {
		if !apierrors.IsNotFound(err) {
			s.c.log().Warn("reading a changed HostProcess", "name", name, "err", err.Error())
		}
		return
	}
	s.reconcileObject(ctx, *cur)
}

func (c Config) log() *slog.Logger {
	if c.Log == nil {
		return slog.New(slog.DiscardHandler)
	}
	if c.Name != "" {
		return c.Log.With("component", "hostprocess", "supervisor", c.Name)
	}
	return c.Log.With("component", "hostprocess")
}

func (c Config) prefixes() []string {
	if c.Prefix == "" {
		return c.Prefixes
	}
	return append([]string{c.Prefix}, c.Prefixes...)
}

func (c Config) prefixOf(unit string) string {
	ps := c.prefixes()
	for _, p := range ps {
		if strings.HasPrefix(unit, p) {
			return p
		}
	}
	if len(ps) == 0 {
		return ""
	}
	return ps[0]
}

func (c Config) workspace() string {
	if c.Workspace == "" {
		return DefaultWorkspace
	}
	return c.Workspace
}

func applySets(ctx context.Context, c Config, restCfg *rest.Config) error {
	ap, err := kcpapply.New(restCfg, kcpapply.Options{WaitTimeout: c.ApplyTimeout, Base: c.workspace(), Log: c.log()})
	if err != nil {
		return err
	}
	ix, err := manifests.Load()
	if err != nil {
		return err
	}
	for _, name := range []string{ProviderSet, ManifestSet} {
		set, ok := ix.Set(name)
		if !ok {
			return fmt.Errorf("hostprocess: deploy/sets.yaml names no %s set", name)
		}
		values, err := manifests.DecodeValues(set.Values, nil)
		if err != nil {
			return err
		}
		if _, err := ap.Apply(ctx, set, values); err != nil {
			return fmt.Errorf("hostprocess: applying the %s set: %w", name, err)
		}
	}
	return nil
}

func New(ctx context.Context, c Config) (*Supervisor, error) {
	restCfg, err := kcpserver.FrontRestConfig(c.Kubeconfig)
	if err != nil {
		return nil, err
	}
	if !c.Bound {
		if err := applySets(ctx, c, restCfg); err != nil {
			return nil, err
		}
	}
	cfg := rest.CopyConfig(restCfg)
	cfg.Host = strings.TrimSuffix(cfg.Host, "/") + "/clusters/" + c.workspace()
	kc, err := kube.New(cfg)
	if err != nil {
		return nil, err
	}
	return &Supervisor{c: c, client: kc.Resource(GVR), placed: map[string]bool{}, backend: c.Manager.Capabilities().Backend,
		files: fswatch.New()}, nil
}

func MirrorName(role string) string { return MirrorPrefix + role }

func (s *Supervisor) syncMirrors(ctx context.Context) {
	if s.c.Mirrors == nil {
		return
	}
	for _, m := range s.c.Mirrors() {
		name := MirrorName(m.Role)
		want := v1alpha1.HostProcessSpec{Unit: m.Unit, Placement: v1alpha1.HostProcessPlacementBootstrap, Slice: m.Slice,
			After: m.After, Wants: m.Wants}
		if len(m.Exec) > 0 {
			want.Exec, want.Argv = m.Exec[0], m.Exec[1:]
		}
		cur, err := kube.Get[v1alpha1.HostProcess](ctx, s.client, name)
		if apierrors.IsNotFound(err) {
			o := v1alpha1.HostProcess{ObjectMeta: metav1.ObjectMeta{Name: name, Labels: map[string]string{MirrorLabel: string(v1alpha1.HostProcessPlacementBootstrap)}}, Spec: want}
			if _, err := kube.Create[v1alpha1.HostProcess](ctx, s.client, &o, metav1.CreateOptions{}); err != nil {
				s.c.log().Warn("writing a bootstrap mirror", "name", name, "err", err.Error())
			}
			continue
		}
		if err != nil {
			s.c.log().Warn("reading a bootstrap mirror", "name", name, "err", err.Error())
			continue
		}
		o := *cur
		if o.Spec.Unit != want.Unit || o.Spec.Exec != want.Exec || !slices.Equal(o.Spec.Argv, want.Argv) || o.Spec.Placement != want.Placement {
			o.Spec = want
			if _, err := kube.Update[v1alpha1.HostProcess](ctx, s.client, &o, metav1.UpdateOptions{}); err != nil {
				s.c.log().Warn("updating a bootstrap mirror", "name", name, "err", err.Error())
			}
		}
	}
}

func (s *Supervisor) observe(ctx context.Context, o v1alpha1.HostProcess) Observed {
	m := s.c.Manager
	obs := Observed{Object: o, Backend: s.backend, Prefix: s.c.prefixOf(o.Spec.Unit), Deleting: o.DeletionTimestamp != nil}
	if o.Spec.Placement == v1alpha1.HostProcessPlacementBootstrap {
		obs.Prefix = ""
	}
	obs.Unit, obs.Inspect = m.Inspect(ctx, o.Spec.Unit)
	if obs.Inspect != nil {
		return obs
	}
	if o.Spec.Asset != nil && s.c.Resolve != nil {
		if p, err := s.c.Resolve(*o.Spec.Asset); err == nil {
			obs.Exec = p
		}
	}
	rt := o.Status.Runtime
	if rt != nil && rt.InvocationID != "" && rt.Backend == string(s.backend) && o.Spec.Placement != v1alpha1.HostProcessPlacementBootstrap &&
		o.Status.Phase != v1alpha1.HostProcessPhaseLost {
		obs.StartedKey = s.rekeyedFor(o.Spec.Unit)
		if RestartedInPlace(o, obs.Unit) {
			obs.Restarted = true
			return obs
		}
		want := hostsvc.Identity{Unit: o.Spec.Unit, InvocationID: rt.InvocationID, PID: int(rt.Pid), StartTicks: uint64(rt.StartTicks),
			BootID: rt.HostBootID, Backend: hostsvc.Backend(rt.Backend), Manager: rt.Manager}
		c, err := m.Check(ctx, want)
		if err != nil {
			obs.Inspect = err
			return obs
		}
		obs.Check = &c
	}
	return obs
}

func (s *Supervisor) apply(ctx context.Context, o v1alpha1.HostProcess, r Result) (v1alpha1.HostProcess, bool) {
	m := s.c.Manager
	log := s.c.log().With("hostprocess", o.Name, "unit", o.Spec.Unit)
	started, stopFailed, failed := false, false, false
	stopped := map[string]bool{}
	for _, op := range r.Ops {
		if stopFailed {
			break
		}
		switch op.Kind {
		case OpStart:
			wasPlaced := s.isPlaced(op.Unit)
			s.setPlaced(op.Unit, true)
			st, err := m.Start(ctx, op.Spec)
			if err == nil && st.Adopted && st.State != hostsvc.Active && st.State != hostsvc.Activating {
				log.Info("a loaded unit of this name is not running; resetting it before the start", "state", string(st.State), "result", st.Result)
				if err = m.Stop(ctx, op.Unit); err == nil {
					m.ResetFailed(ctx, op.Unit)
					st, err = m.Start(ctx, op.Spec)
				}
			}
			if err != nil {
				r.Phase, r.Message = v1alpha1.HostProcessPhaseFailed, err.Error()
				failed = true
				log.Warn("start", "err", err.Error())
				if !wasPlaced {
					s.setPlaced(op.Unit, false)
				}
				continue
			}
			s.setPlaced(op.Unit, true)
			s.setStartedKey(op.Unit, o.Spec.RestartKey)
			if stopped[op.Unit] {
				s.setRekeyed(op.Unit, o.Spec.RestartKey)
			} else {
				s.setRekeyed(op.Unit, "")
			}
			started = true
			log.Info("placed", "state", string(st.State), "adopted", st.Adopted, "restartKey", o.Spec.RestartKey)
		case OpStop:
			if err := m.Stop(ctx, op.Unit); err != nil {
				log.Warn("stop", "err", err.Error())
				stopFailed, failed = true, true
				continue
			}
			s.setPlaced(op.Unit, false)
			stopped[op.Unit] = true
			log.Info("stopped")
		}
	}
	next := o
	if r.Unknown {
		next.Status.Message = r.Message
		return next, failed
	}
	next.Status.Phase, next.Status.LostReason, next.Status.Message = r.Phase, r.LostReason, r.Message
	next.Status.ObservedGeneration = o.Generation
	if started {
		next.Status.RestartKey = o.Spec.RestartKey
	} else if key, ok := s.startedKey(o.Spec.Unit); ok {
		next.Status.RestartKey = key
	}
	if r.Identify {
		if id, err := m.Identify(ctx, o.Spec.Unit, 0); err == nil {
			st, _ := m.Inspect(ctx, o.Spec.Unit)
			next.Status.Runtime = &v1alpha1.HostProcessRuntime{Backend: string(id.Backend), Unit: id.Unit, InvocationID: id.InvocationID,
				Pid: int64(id.PID), StartTicks: int64(id.StartTicks), HostBootID: id.BootID, Manager: id.Manager}
			if !st.ActiveSince.IsZero() {
				next.Status.Runtime.ActiveSince = st.ActiveSince.UTC().Format(time.RFC3339Nano)
			}
			if o.Spec.Placement == v1alpha1.HostProcessPlacementSupervisor {
				s.setPlaced(o.Spec.Unit, true)
			}
		}
	}
	if next.Status.Runtime != nil && next.Status.Runtime.Pid > 0 && next.Status.Phase == v1alpha1.HostProcessPhaseRunning {
		next.Status.Listeners = listenersOf(int(next.Status.Runtime.Pid))
	} else {
		next.Status.Listeners = nil
	}
	if r.Phase == v1alpha1.HostProcessPhaseStopped || r.Phase == v1alpha1.HostProcessPhaseFailed || r.Phase == v1alpha1.HostProcessPhaseLost {
		if st, err := m.Inspect(ctx, o.Spec.Unit); err == nil && st.State != hostsvc.NotFound {
			next.Status.LastResult = &v1alpha1.HostProcessResult{Result: st.Result, ExitCode: st.ExecMainStatus,
				At: time.Now().UTC().Format(time.RFC3339)}
		}
	}
	conds.SetOn(&next, ConditionReady, next.Status.Phase == v1alpha1.HostProcessPhaseRunning, string(next.Status.Phase), next.Status.Message)
	caps := m.Capabilities()
	conds.SetOn(&next, ConditionLimits, caps.EnforcesLimits, string(caps.Backend), "limits are enforced only where the backend has cgroups")
	return next, failed
}

func (s *Supervisor) writeStatus(ctx context.Context, cur *v1alpha1.HostProcess, next v1alpha1.HostProcess) error {
	next.ResourceVersion = cur.ResourceVersion
	was, err := kube.Encode(cur)
	if err != nil {
		return err
	}
	u, err := kube.Encode(&next)
	if err != nil {
		return err
	}
	if equalStatus(was["status"], u["status"]) {
		return nil
	}
	_, err = kube.UpdateStatus[v1alpha1.HostProcess](ctx, s.client, u, metav1.UpdateOptions{})
	return err
}

func equalStatus(a, b any) bool { return fmt.Sprint(a) == fmt.Sprint(b) }

func (s *Supervisor) Pass(ctx context.Context) error {
	s.syncMirrors(ctx)
	listedAt := time.Now()
	list, err := kube.List[v1alpha1.HostProcess](ctx, s.client, metav1.ListOptions{LabelSelector: s.c.Selector})
	if err != nil {
		return err
	}
	seen := map[string]bool{}
	var liveSlices []string
	for i := range list {
		if o, ok := s.reconcileObject(ctx, list[i]); ok {
			seen[o.Spec.Unit] = true
			liveSlices = append(liveSlices, o.Spec.Slice)
		}
	}
	for _, prefix := range s.c.prefixes() {
		if s.c.KeepOrphans {
			break
		}
		units, err := s.c.Manager.List(ctx, prefix)
		if err == nil {
			for _, u := range units {
				if !seen[u.Name] && u.State == hostsvc.Failed && !s.placedAfter(u.Name, listedAt) {
					s.c.log().Info("the HostProcess of a failed unit is gone; resetting the unit", "unit", u.Name)
					if err := s.c.Manager.ResetFailed(ctx, u.Name); err != nil {
						s.c.log().Warn("resetting the failed unit of a gone HostProcess failed; the next pass retries", "unit", u.Name, "err", err.Error())
					} else {
						s.setPlaced(u.Name, false)
					}
					continue
				}
				if !s.isPlaced(u.Name) && s.listed {
					continue
				}
				if !seen[u.Name] && s.placedAfter(u.Name, listedAt) {
					continue
				}
				if !seen[u.Name] && (u.State == hostsvc.Active || u.State == hostsvc.Activating) {
					s.c.log().Info("the HostProcess of a placed unit is gone; stopping the unit", "unit", u.Name)
					if err := s.c.Manager.Stop(ctx, u.Name); err != nil {
						s.c.log().Warn("stopping the unit of a gone HostProcess failed; the next pass retries", "unit", u.Name, "err", err.Error())
					} else {
						s.setPlaced(u.Name, false)
					}
				}
			}
		}
	}
	s.listed = true
	if !s.c.KeepOrphans {
		s.sweepSlices(ctx, liveSlices)
	}
	return nil
}

const ClusterSlicePart = "cluster-"

func (s *Supervisor) sweepable(slice string) bool {
	if hostsvc.ValidateSlice(slice) != nil {
		return false
	}
	for _, p := range s.c.prefixes() {
		if strings.HasPrefix(slice, p+ClusterSlicePart) {
			return true
		}
	}
	return false
}

func (s *Supervisor) sweepSlices(ctx context.Context, live []string) {
	s.mu.Lock()
	if s.slices == nil {
		s.slices = map[string]bool{}
	}
	for _, sl := range live {
		if s.sweepable(sl) {
			s.slices[sl] = true
		}
	}
	var empty []string
	for sl := range s.slices {
		held := false
		for _, l := range live {
			if hostsvc.SliceContains(sl, l) {
				held = true
				break
			}
		}
		if !held {
			empty = append(empty, sl)
		}
	}
	s.mu.Unlock()
	for _, sl := range empty {
		if err := s.c.Manager.StopSlice(ctx, sl); err != nil {
			s.c.log().Warn("stopping the empty slice of gone HostProcesses failed; the next pass retries", "slice", sl, "err", err.Error())
			continue
		}
		s.mu.Lock()
		delete(s.slices, sl)
		s.mu.Unlock()
		s.c.log().Info("every HostProcess of the slice is gone; the empty slice is stopped", "slice", sl)
	}
}

func outputFiles(o v1alpha1.HostProcess) ([]string, int64) {
	out := o.Spec.Output
	if out == nil {
		return nil, 0
	}
	var files []string
	for _, f := range []string{out.Stdout, out.Stderr} {
		if f != "" && !slices.Contains(files, f) {
			files = append(files, f)
		}
	}
	return files, cmp.Or(out.CapBytes, logcap.DefaultCap)
}

const OutputDebounce = time.Second

func (s *Supervisor) followOutput(ctx context.Context, o v1alpha1.HostProcess) {
	files, capBytes := outputFiles(o)
	key := ""
	if o.DeletionTimestamp == nil && o.Status.Phase == v1alpha1.HostProcessPhaseRunning && len(files) > 0 {
		key = strings.Join(files, "\n") + "\n" + strconv.FormatInt(capBytes, 10)
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if cur, ok := s.outputs[o.Name]; ok {
		if cur.key == key {
			return
		}
		cur.stop()
		delete(s.outputs, o.Name)
	}
	if key == "" {
		return
	}
	w, err := s.files.Subscribe(files...)
	if err != nil {
		s.c.log().Warn("output files are capped only at reconcile: watching an output file failed", "name", o.Name, "files", strings.Join(files, ", "), "err", err.Error())
		return
	}
	fctx, cancel := context.WithCancel(ctx)
	if s.outputs == nil {
		s.outputs = map[string]follower{}
	}
	s.outputs[o.Name] = follower{key: key, stop: cancel}
	s.follows.Go(func() { s.rotateOnWrite(fctx, w, o, key) })
}

func (s *Supervisor) unfollowOutput(name string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if cur, ok := s.outputs[name]; ok {
		cur.stop()
		delete(s.outputs, name)
	}
}

func (s *Supervisor) rotateOnWrite(ctx context.Context, w *fswatch.Sub, o v1alpha1.HostProcess, key string) {
	defer w.Close()
	defer func() {
		s.mu.Lock()
		defer s.mu.Unlock()
		if cur, ok := s.outputs[o.Name]; ok && cur.key == key && ctx.Err() == nil {
			cur.stop()
			delete(s.outputs, o.Name)
		}
	}()
	for {
		select {
		case <-ctx.Done():
			return
		case _, ok := <-w.Errors():
			if !ok {
				return
			}
		case ev, ok := <-w.Events():
			if !ok || ev.Has(fsnotify.Remove) || ev.Has(fsnotify.Rename) {
				return
			}
		}
		unlock := s.lockObject(o.Name)
		s.capOutput(o)
		unlock()
		select {
		case <-ctx.Done():
			return
		case <-time.After(OutputDebounce):
		}
	}
}

func (s *Supervisor) capOutput(o v1alpha1.HostProcess) {
	files, capBytes := outputFiles(o)
	for _, f := range files {
		ok, err := logcap.Rotate(f, capBytes)
		if err != nil {
			s.c.log().Warn("an output file over its cap was not rotated", "name", o.Name, "file", f, "err", err.Error())
		}
		if ok {
			s.c.log().Info("an output file over its cap was rotated; it keeps its newest bytes in a .1 copy", "name", o.Name, "file", f, "cap", capBytes)
		}
	}
}

func (s *Supervisor) Run(ctx context.Context) error {
	s.c.log().Info("the HostProcess supervisor runs in the core", "workspace", s.c.workspace(), "backend", string(s.backend),
		"prefixes", s.c.prefixes())
	defer s.files.Close()
	defer s.follows.Wait()
	wake := make(chan struct{}, 1)
	changed := make(chan string, 1024)
	s.mu.Lock()
	s.changed = changed
	s.mu.Unlock()
	var wg sync.WaitGroup
	defer wg.Wait()
	for range EventWorkers {
		wg.Go(func() {
			for {
				select {
				case <-ctx.Done():
					return
				case name := <-changed:
					s.reconcileName(ctx, name)
				}
			}
		})
	}
	wg.Go(func() { s.watch(ctx, wake, changed) })
	for _, prefix := range s.c.prefixes() {
		wg.Go(func() { s.watchUnits(ctx, prefix, wake, changed) })
	}
	for {
		if err := s.Pass(ctx); err != nil {
			s.c.log().Warn("a reconcile pass failed; units keep running and the pass retries", "err", err.Error(), "after", RetryAfter.String())
			time.AfterFunc(RetryAfter, func() { poke(wake) })
		}
		select {
		case <-ctx.Done():
			return nil
		case <-wake:
			select {
			case <-ctx.Done():
				return nil
			case <-time.After(WakeDebounce):
			}
			select {
			case <-wake:
			default:
			}
		}
	}
}

const WakeDebounce = 100 * time.Millisecond

const EventWorkers = 4

func poke(wake chan<- struct{}) {
	select {
	case wake <- struct{}{}:
	default:
	}
}

func (s *Supervisor) watch(ctx context.Context, wake chan<- struct{}, changedCh chan<- string) {
	generations := map[string]int64{}
	failing := false
	for ctx.Err() == nil {
		events, stop, err := kube.Watch[v1alpha1.HostProcess](ctx, s.client, metav1.ListOptions{LabelSelector: s.c.Selector})
		if err != nil {
			if !failing {
				s.c.log().Warn("watching HostProcess objects failed; the supervisor retries the watch", "err", err.Error(), "after", RetryAfter.String())
				failing = true
			}
			select {
			case <-ctx.Done():
				return
			case <-time.After(RetryAfter):
			}
			continue
		}
		if failing {
			s.c.log().Info("watching HostProcess objects again")
			failing = false
		}
		poke(wake)
		for ev := range events {
			if ev.Object == nil {
				continue
			}
			name, gen, unit := ev.Object.Name, ev.Object.Generation, ev.Object.Spec.Unit
			s.track(name, unit, ev.Type == watch.Deleted)
			if ev.Type == watch.Deleted {
				s.unfollowOutput(name)
			}
			changed := ev.Type != watch.Modified || generations[name] != gen
			if ev.Type == watch.Deleted {
				delete(generations, name)
			} else {
				generations[name] = gen
			}
			if !changed {
				continue
			}
			if ev.Type != watch.Deleted {
				select {
				case changedCh <- name:
					continue
				default:
				}
			}
			poke(wake)
		}
		stop()
		select {
		case <-ctx.Done():
			return
		case <-time.After(RetryAfter):
		}
	}
}

func (s *Supervisor) watchUnits(ctx context.Context, prefix string, wake chan<- struct{}, changed chan<- string) {
	failing := false
	for ctx.Err() == nil {
		events, err := s.c.Manager.Watch(ctx, prefix)
		if err != nil {
			if !failing {
				s.c.log().Warn("watching units failed; the supervisor retries the watch", "prefix", prefix, "err", err.Error(), "after", RetryAfter.String())
				failing = true
			}
			select {
			case <-ctx.Done():
				return
			case <-time.After(RetryAfter):
			}
			continue
		}
		if failing {
			s.c.log().Info("watching units again", "prefix", prefix)
			failing = false
		}
		poke(wake)
		for ev := range events {
			name, ok := s.objectOf(ev.Name)
			if !ok {
				if !s.c.KeepOrphans {
					poke(wake)
				}
				continue
			}
			select {
			case changed <- name:
			default:
				poke(wake)
			}
		}
		select {
		case <-ctx.Done():
			return
		case <-time.After(RetryAfter):
		}
	}
}

func Supervise(ctx context.Context, c Config) {
	log := c.log()
	supervise.Restart(ctx, "hostprocess", supervise.Backoff{Min: RetryAfter, Max: RetryAfter, OnError: func(err error, after time.Duration) {
		log.Error("the HostProcess supervisor stopped; starting it again", "err", fmt.Sprint(err), "after", after.String())
	}}, func(ctx context.Context) error {
		s, err := New(ctx, c)
		if err != nil {
			return err
		}
		return s.Run(ctx)
	})
}

func listenersOf(pid int) []v1alpha1.HostProcessListener {
	ports, err := hostsvc.ListenPorts(pid)
	if err != nil || len(ports) == 0 {
		return nil
	}
	if len(ports) == 1 {
		return []v1alpha1.HostProcessListener{{Name: "tcp", Address: "127.0.0.1:" + strconv.Itoa(ports[0])}}
	}
	out := make([]v1alpha1.HostProcessListener, 0, len(ports))
	for _, p := range ports {
		out = append(out, v1alpha1.HostProcessListener{Name: "tcp-" + strconv.Itoa(p), Address: "127.0.0.1:" + strconv.Itoa(p)})
	}
	return out
}