Files
hakurei/container/initdaemon.go
Ophestra 3d1e47f0aa
All checks were successful
Test / Create distribution (push) Successful in 41s
Test / Sandbox (push) Successful in 2m46s
Test / Sandbox (race detector) (push) Successful in 4m54s
Test / Hpkg (push) Successful in 5m14s
Test / Hakurei (push) Successful in 5m43s
Test / Hakurei (race detector) (push) Successful in 7m10s
Test / Flake checks (push) Successful in 1m32s
container: start daemons within container
This is useful for daemons internal to the container. The only current use case is pipewire-pulse.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 06:35:36 +09:00

95 lines
2.6 KiB
Go

package container
import (
"context"
"encoding/gob"
"errors"
"fmt"
"os"
"os/exec"
"slices"
"syscall"
"time"
"hakurei.app/container/check"
"hakurei.app/container/fhs"
)
func init() { gob.Register(new(DaemonOp)) }
const (
// daemonTimeout is the duration a [DaemonOp] is allowed to block before the
// [DaemonOp.Target] marker becomes available.
daemonTimeout = 5 * time.Second
)
// Daemon appends an [Op] that starts a daemon in the container and blocks until
// [DaemonOp.Target] appears.
func (f *Ops) Daemon(target *check.Absolute, silent bool, path *check.Absolute, args ...string) *Ops {
*f = append(*f, &DaemonOp{target, silent, path, args})
return f
}
// DaemonOp starts a daemon in the container and blocks until Target appears.
type DaemonOp struct {
// Pathname indicating readiness of daemon.
Target *check.Absolute
// Whether output is silenced.
Silent bool
// Absolute pathname passed to [exec.Cmd].
Path *check.Absolute
// Arguments (excl. first) passed to [exec.Cmd].
Args []string
}
func (d *DaemonOp) Valid() bool { return d != nil && d.Target != nil && d.Path != nil }
func (d *DaemonOp) early(*setupState, syscallDispatcher) error { return nil }
func (d *DaemonOp) apply(*setupState, syscallDispatcher) error { return nil }
func (d *DaemonOp) late(state *setupState, k syscallDispatcher) error {
cmd := exec.CommandContext(state.Context, d.Path.String(), d.Args...)
cmd.Env = state.Env
cmd.Dir = fhs.Root
if !d.Silent {
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
}
// WaitDelay: left unset because lifetime is bound by AdoptWaitDelay on cancellation
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) }
state.Verbosef("starting %s", d.String())
if err := k.start(cmd); err != nil {
return err
}
// Wait: reaped by wait4 loop
deadline := time.Now().Add(daemonTimeout)
for {
if _, err := k.stat(d.Target.String()); err != nil {
if !errors.Is(err, os.ErrNotExist) {
_ = k.signal(cmd, os.Kill)
return err
}
if time.Now().After(deadline) {
_ = k.signal(cmd, os.Kill)
return context.DeadlineExceeded
}
time.Sleep(500 * time.Microsecond)
continue
}
state.Verbosef("daemon process %d ready", cmd.Process.Pid)
return nil
}
}
func (d *DaemonOp) Is(op Op) bool {
vd, ok := op.(*DaemonOp)
return ok && d.Valid() && vd.Valid() &&
d.Target.Is(vd.Target) && d.Silent == vd.Silent &&
d.Path.Is(vd.Path) && slices.Equal(d.Args, vd.Args)
}
func (*DaemonOp) prefix() (string, bool) { return zeroString, false }
func (d *DaemonOp) String() string { return fmt.Sprintf("daemon providing %q", d.Target) }