forked from security/hakurei
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>
135 lines
3.7 KiB
Go
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) }
|