All checks were successful
Test / Create distribution (push) Successful in 1m16s
Test / Sandbox (push) Successful in 3m2s
Test / Hakurei (push) Successful in 4m4s
Test / ShareFS (push) Successful in 4m17s
Test / Hpkg (push) Successful in 4m49s
Test / Sandbox (race detector) (push) Successful in 5m22s
Test / Hakurei (race detector) (push) Successful in 6m30s
Test / Flake checks (push) Successful in 1m48s
This change removes inconsistencies collected over time in this package. Signed-off-by: Ophestra <cat@gensokyo.uk>
134 lines
3.6 KiB
Go
134 lines
3.6 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 is a helper for appending [DaemonOp] to [Ops].
|
|
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) }
|