diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-12-08 06:35:36 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-12-08 07:21:04 +0900 |
| commit | 357cfcddeea14d6babacf7ae8f77e0bad7b24ee5 (patch) | |
| tree | eaad515e98541dd3e3f6d0bdfc5f54d360463f48 /container/initdaemon.go | |
| parent | 6bf245cf1b8d5e30c051bb6295533d287cb4eda6 (diff) | |
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>
Diffstat (limited to 'container/initdaemon.go')
| -rw-r--r-- | container/initdaemon.go | 105 |
1 files changed, 105 insertions, 0 deletions
diff --git a/container/initdaemon.go b/container/initdaemon.go new file mode 100644 index 00000000..ee360dc9 --- /dev/null +++ b/container/initdaemon.go @@ -0,0 +1,105 @@ +package container + +import ( + "context" + "encoding/gob" + "errors" + "fmt" + "os" + "os/exec" + "slices" + "sync/atomic" + "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, path *check.Absolute, args ...string) *Ops { + *f = append(*f, &DaemonOp{target, 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 + // 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 state.IsVerbose() { + 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 + } + + var done atomic.Pointer[error] + k.new(func(k syscallDispatcher) { + err := k.wait(cmd) + done.Store(&err) + + if err != nil { + state.Verbosef("%s %v", d.String(), err) + } + }) + + 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 + } + + if errP := done.Load(); errP != nil { + return *errP + } + + 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.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) } |
