container: start daemons within container
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m37s
Test / Sandbox (race detector) (push) Successful in 4m44s
Test / Hakurei (push) Successful in 5m5s
Test / Hpkg (push) Successful in 5m2s
Test / Hakurei (race detector) (push) Successful in 6m26s
Test / Flake checks (push) Successful in 1m29s
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m37s
Test / Sandbox (race detector) (push) Successful in 4m44s
Test / Hakurei (push) Successful in 5m5s
Test / Hpkg (push) Successful in 5m2s
Test / Hakurei (race detector) (push) Successful in 6m26s
Test / Flake checks (push) Successful in 1m29s
This is useful for daemons internal to the container. The only current use case is pipewire-pulse. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
@@ -759,7 +759,8 @@ func (k *kstub) checkMsg(msg message.Msg) {
|
||||
}
|
||||
|
||||
func (k *kstub) GetLogger() *log.Logger { panic("unreachable") }
|
||||
func (k *kstub) IsVerbose() bool { panic("unreachable") }
|
||||
|
||||
func (k *kstub) IsVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) }
|
||||
|
||||
func (k *kstub) SwapVerbose(verbose bool) bool {
|
||||
k.Helper()
|
||||
|
||||
91
container/initdaemon.go
Normal file
91
container/initdaemon.go
Normal file
@@ -0,0 +1,91 @@
|
||||
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, 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
|
||||
}
|
||||
// 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.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) }
|
||||
99
container/initdaemon_test.go
Normal file
99
container/initdaemon_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/container/stub"
|
||||
)
|
||||
|
||||
func TestDaemonOp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
checkSimple(t, "DaemonOp.late", []simpleTestCase{
|
||||
{"success", func(k *kstub) error {
|
||||
state := setupState{Params: &Params{Env: []string{"\x00"}}, Context: t.Context(), Msg: k}
|
||||
return (&DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
}).late(&state, k)
|
||||
}, stub.Expect{Calls: []stub.Call{
|
||||
call("isVerbose", stub.ExpectArgs{}, true, nil),
|
||||
call("verbosef", stub.ExpectArgs{"starting %s", []any{`daemon providing "/run/user/1971/pulse/native"`}}, nil, nil),
|
||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/pipewire-pulse", []string{"/run/current-system/sw/bin/pipewire-pulse", "-v"}, []string{"\x00"}, "/"}, &os.Process{Pid: 0xcafe}, nil),
|
||||
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
|
||||
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
|
||||
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
|
||||
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), nil),
|
||||
call("verbosef", stub.ExpectArgs{"daemon process %d ready", []any{0xcafe}}, nil, nil),
|
||||
}}, nil},
|
||||
})
|
||||
|
||||
checkOpsValid(t, []opValidTestCase{
|
||||
{"nil", (*DaemonOp)(nil), false},
|
||||
{"zero", new(DaemonOp), false},
|
||||
{"valid", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||
{"pipewire-pulse", new(Ops).Daemon(
|
||||
check.MustAbs("/run/user/1971/pulse/native"),
|
||||
check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"), "-v",
|
||||
), Ops{
|
||||
&DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
},
|
||||
}},
|
||||
})
|
||||
|
||||
checkOpIs(t, []opIsTestCase{
|
||||
{"zero", new(DaemonOp), new(DaemonOp), false},
|
||||
|
||||
{"args differs", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
Args: []string{"-v"},
|
||||
}, &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, false},
|
||||
|
||||
{"path differs", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire"),
|
||||
}, &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, false},
|
||||
|
||||
{"target differs", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/65534/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, false},
|
||||
|
||||
{"equals", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"pipewire-pulse", &DaemonOp{
|
||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
||||
}, zeroString, `daemon providing "/run/user/1971/pulse/native"`},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user