Files
hakurei/container/initdaemon.go
Ophestra e9fb1d7be5 container/initdaemon: copy wstatus from wait4 loop
Due to the special nature of the init process, direct use of wait outside the wait4 loop is racy. This change copies the wstatus from wait4 loop state instead.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-12-08 22:58:42 +09:00

135 lines
3.7 KiB
Go

package container
import (
"context"
"encoding/gob"
"errors"
"fmt"
"os"
"os/exec"
"slices"
"strconv"
"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
}
// earlyTerminationError is returned by [DaemonOp] when a daemon terminates
// before [DaemonOp.Target] appears.
type earlyTerminationError struct {
// Returned by [DaemonOp.String].
op string
// Copied from wait4 loop.
wstatus syscall.WaitStatus
}
func (e *earlyTerminationError) Error() string {
res := ""
switch {
case e.wstatus.Exited():
res = "exit status " + strconv.Itoa(e.wstatus.ExitStatus())
case e.wstatus.Signaled():
res = "signal: " + e.wstatus.Signal().String()
case e.wstatus.Stopped():
res = "stop signal: " + e.wstatus.StopSignal().String()
if e.wstatus.StopSignal() == syscall.SIGTRAP && e.wstatus.TrapCause() != 0 {
res += " (trap " + strconv.Itoa(e.wstatus.TrapCause()) + ")"
}
case e.wstatus.Continued():
res = "continued"
}
if e.wstatus.CoreDump() {
res += " (core dumped)"
}
return res
}
func (e *earlyTerminationError) Message() string { return e.op + " " + e.Error() }
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
}
deadline := time.Now().Add(daemonTimeout)
var wstatusErr error
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 wstatusErr != nil {
return wstatusErr
}
if wstatus, ok := state.terminated(cmd.Process.Pid); ok {
// check once again: process could have satisfied Target between stat and the lookup
wstatusErr = &earlyTerminationError{d.String(), wstatus}
continue
}
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) }