container/params: expose pipe
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 43s
Test / Sandbox (race detector) (push) Successful in 2m12s
Test / Hakurei (push) Successful in 2m22s
Test / Hakurei (race detector) (push) Successful in 3m5s
Test / Hpkg (push) Successful in 3m26s
Test / Flake checks (push) Successful in 1m40s

This increases flexibility of how caller wants to handle the I/O. Also makes it no longer rely on finalizer.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-10-31 22:34:38 +09:00
parent 6a0ecced90
commit 1196200121
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
4 changed files with 45 additions and 39 deletions

View File

@ -25,6 +25,9 @@ const (
// CancelSignal is the signal expected by container init on context cancel. // CancelSignal is the signal expected by container init on context cancel.
// A custom [Container.Cancel] function must eventually deliver this signal. // A custom [Container.Cancel] function must eventually deliver this signal.
CancelSignal = SIGUSR2 CancelSignal = SIGUSR2
// Timeout for writing initParams to Container.setup.
initSetupTimeout = 5 * time.Second
) )
type ( type (
@ -37,8 +40,8 @@ type (
// with behaviour identical to its [exec.Cmd] counterpart. // with behaviour identical to its [exec.Cmd] counterpart.
ExtraFiles []*os.File ExtraFiles []*os.File
// param encoder for shim and init // param pipe for shim and init
setup *gob.Encoder setup *os.File
// cancels cmd // cancels cmd
cancel context.CancelFunc cancel context.CancelFunc
// closed after Wait returns // closed after Wait returns
@ -228,10 +231,10 @@ func (p *Container) Start() error {
} }
// place setup pipe before user supplied extra files, this is later restored by init // place setup pipe before user supplied extra files, this is later restored by init
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil { if fd, f, err := Setup(&p.cmd.ExtraFiles); err != nil {
return &StartError{true, "set up params stream", err, false, false} return &StartError{true, "set up params stream", err, false, false}
} else { } else {
p.setup = e p.setup = f
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)} p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
} }
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...) p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
@ -310,6 +313,9 @@ func (p *Container) Serve() error {
setup := p.setup setup := p.setup
p.setup = nil p.setup = nil
if err := setup.SetDeadline(time.Now().Add(initSetupTimeout)); err != nil {
return &StartError{true, "set init pipe deadline", err, false, true}
}
if p.Path == nil { if p.Path == nil {
p.cancel() p.cancel()
@ -324,15 +330,14 @@ func (p *Container) Serve() error {
p.SeccompRules = make([]seccomp.NativeRule, 0) p.SeccompRules = make([]seccomp.NativeRule, 0)
} }
err := setup.Encode( err := gob.NewEncoder(setup).Encode(&initParams{
&initParams{
p.Params, p.Params,
Getuid(), Getuid(),
Getgid(), Getgid(),
len(p.ExtraFiles), len(p.ExtraFiles),
p.msg.IsVerbose(), p.msg.IsVerbose(),
}, })
) _ = setup.Close()
if err != nil { if err != nil {
p.cancel() p.cancel()
} }

View File

@ -9,13 +9,13 @@ import (
) )
// Setup appends the read end of a pipe for setup params transmission and returns its fd. // Setup appends the read end of a pipe for setup params transmission and returns its fd.
func Setup(extraFiles *[]*os.File) (int, *gob.Encoder, error) { func Setup(extraFiles *[]*os.File) (int, *os.File, error) {
if r, w, err := os.Pipe(); err != nil { if r, w, err := os.Pipe(); err != nil {
return -1, nil, err return -1, nil, err
} else { } else {
fd := 3 + len(*extraFiles) fd := 3 + len(*extraFiles)
*extraFiles = append(*extraFiles, r) *extraFiles = append(*extraFiles, r)
return fd, gob.NewEncoder(w), nil return fd, w, nil
} }
} }

View File

@ -1,6 +1,7 @@
package container_test package container_test
import ( import (
"encoding/gob"
"errors" "errors"
"os" "os"
"slices" "slices"
@ -59,12 +60,16 @@ func TestSetupReceive(t *testing.T) {
encoderDone := make(chan error, 1) encoderDone := make(chan error, 1)
extraFiles := make([]*os.File, 0, 1) extraFiles := make([]*os.File, 0, 1)
if fd, encoder, err := container.Setup(&extraFiles); err != nil { deadline, _ := t.Deadline()
if fd, f, err := container.Setup(&extraFiles); err != nil {
t.Fatalf("Setup: error = %v", err) t.Fatalf("Setup: error = %v", err)
} else if fd != 3 { } else if fd != 3 {
t.Fatalf("Setup: fd = %d, want 3", fd) t.Fatalf("Setup: fd = %d, want 3", fd)
} else { } else {
go func() { encoderDone <- encoder.Encode(payload) }() if err = f.SetDeadline(deadline); err != nil {
t.Fatal(err.Error())
}
go func() { encoderDone <- gob.NewEncoder(f).Encode(payload) }()
} }
if len(extraFiles) != 1 { if len(extraFiles) != 1 {

View File

@ -20,8 +20,12 @@ import (
"hakurei.app/system" "hakurei.app/system"
) )
const (
// Duration to wait for shim to exit on top of container WaitDelay. // Duration to wait for shim to exit on top of container WaitDelay.
const shimWaitTimeout = 5 * time.Second shimWaitTimeout = 5 * time.Second
// Timeout from setup pipe creation to when outcomeState is fully written.
shimSetupTimeout = 5 * time.Second
)
// mainState holds persistent state bound to outcome.main. // mainState holds persistent state bound to outcome.main.
type mainState struct { type mainState struct {
@ -214,7 +218,7 @@ func (k *outcome) main(msg message.Msg) {
hsuPath := internal.MustHsuPath() hsuPath := internal.MustHsuPath()
// ms.beforeExit required beyond this point // ms.beforeExit required beyond this point
ms := &mainState{Msg: msg, k: k} ms := mainState{Msg: msg, k: k}
if err := k.sys.Commit(); err != nil { if err := k.sys.Commit(); err != nil {
ms.fatal("cannot commit system setup:", err) ms.fatal("cannot commit system setup:", err)
@ -232,11 +236,12 @@ func (k *outcome) main(msg message.Msg) {
// shim runs in the same session as monitor; see shim.go for behaviour // shim runs in the same session as monitor; see shim.go for behaviour
ms.cmd.Cancel = func() error { return ms.cmd.Process.Signal(syscall.SIGCONT) } ms.cmd.Cancel = func() error { return ms.cmd.Process.Signal(syscall.SIGCONT) }
var e *gob.Encoder var shimPipe *os.File
if fd, encoder, err := container.Setup(&ms.cmd.ExtraFiles); err != nil { if fd, w, err := container.Setup(&ms.cmd.ExtraFiles); err != nil {
ms.fatal("cannot create shim setup pipe:", err) ms.fatal("cannot create shim setup pipe:", err)
panic("unreachable")
} else { } else {
e = encoder shimPipe = w
ms.cmd.Env = []string{ ms.cmd.Env = []string{
// passed through to shim by hsu // passed through to shim by hsu
shimEnv + "=" + strconv.Itoa(fd), shimEnv + "=" + strconv.Itoa(fd),
@ -262,23 +267,14 @@ func (k *outcome) main(msg message.Msg) {
go func() { ms.cmdWait <- ms.cmd.Wait(); cancel() }() go func() { ms.cmdWait <- ms.cmd.Wait(); cancel() }()
ms.Time = &startTime ms.Time = &startTime
// unfortunately the I/O here cannot be directly canceled; if err := shimPipe.SetDeadline(time.Now().Add(shimSetupTimeout)); err != nil {
// the cancellation path leads to fatal in this case so that is fine msg.Verbose(err.Error())
select { }
case err := <-func() (setupErr chan error) { if err := gob.NewEncoder(shimPipe).Encode(k.state); err != nil {
setupErr = make(chan error, 1)
go func() { setupErr <- e.Encode(k.state) }()
return
}():
if err != nil {
msg.Resume() msg.Resume()
ms.fatal("cannot transmit shim config:", err) ms.fatal("cannot transmit shim config:", err)
} }
_ = shimPipe.Close()
case <-ctx.Done():
msg.Resume()
ms.fatal("shim context canceled:", newWithMessageError("shim setup canceled", ctx.Err()))
}
// shim accepted setup payload, create process state // shim accepted setup payload, create process state
if ok, err := ms.store.Do(k.state.identity.unwrap(), func(c store.Cursor) { if ok, err := ms.store.Do(k.state.identity.unwrap(), func(c store.Cursor) {