container: forward context cancellation
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m56s
Test / Hakurei (push) Successful in 2m47s
Test / Planterette (push) Successful in 3m40s
Test / Sandbox (race detector) (push) Successful in 3m45s
Test / Hakurei (race detector) (push) Successful in 4m29s
Test / Flake checks (push) Successful in 1m18s
All checks were successful
Test / Create distribution (push) Successful in 32s
Test / Sandbox (push) Successful in 1m56s
Test / Hakurei (push) Successful in 2m47s
Test / Planterette (push) Successful in 3m40s
Test / Sandbox (race detector) (push) Successful in 3m45s
Test / Hakurei (race detector) (push) Successful in 4m29s
Test / Flake checks (push) Successful in 1m18s
This allows container processes to exit gracefully. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
parent
65fe09caf9
commit
d6b07f12ff
@ -21,6 +21,10 @@ const (
|
|||||||
// Nonexistent is a path that cannot exist.
|
// Nonexistent is a path that cannot exist.
|
||||||
// /proc is chosen because a system with covered /proc is unsupported by this package.
|
// /proc is chosen because a system with covered /proc is unsupported by this package.
|
||||||
Nonexistent = "/proc/nonexistent"
|
Nonexistent = "/proc/nonexistent"
|
||||||
|
|
||||||
|
// CancelSignal is the signal expected by container init on context cancel.
|
||||||
|
// A custom [Container.Cancel] function must eventually deliver this signal.
|
||||||
|
CancelSignal = SIGTERM
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@ -62,6 +66,8 @@ type (
|
|||||||
Path string
|
Path string
|
||||||
// Initial process argv.
|
// Initial process argv.
|
||||||
Args []string
|
Args []string
|
||||||
|
// Deliver SIGINT to the initial process on context cancellation.
|
||||||
|
ForwardCancel bool
|
||||||
|
|
||||||
// Mapped Uid in user namespace.
|
// Mapped Uid in user namespace.
|
||||||
Uid int
|
Uid int
|
||||||
@ -129,7 +135,7 @@ func (p *Container) Start() error {
|
|||||||
if p.Cancel != nil {
|
if p.Cancel != nil {
|
||||||
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
|
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
|
||||||
} else {
|
} else {
|
||||||
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(SIGTERM) }
|
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
|
||||||
}
|
}
|
||||||
p.cmd.Dir = "/"
|
p.cmd.Dir = "/"
|
||||||
p.cmd.SysProcAttr = &SysProcAttr{
|
p.cmd.SysProcAttr = &SysProcAttr{
|
||||||
@ -226,6 +232,14 @@ func (p *Container) String() string {
|
|||||||
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
|
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProcessState returns the address to os.ProcessState held by the underlying [exec.Cmd].
|
||||||
|
func (p *Container) ProcessState() *os.ProcessState {
|
||||||
|
if p.cmd == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.cmd.ProcessState
|
||||||
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, name string, args ...string) *Container {
|
func New(ctx context.Context, name string, args ...string) *Container {
|
||||||
return &Container{name: name, ctx: ctx,
|
return &Container{name: name, ctx: ctx,
|
||||||
Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
|
Params: Params{Args: append([]string{name}, args...), Dir: "/", Ops: new(Ops)},
|
||||||
|
@ -8,6 +8,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
@ -90,41 +92,32 @@ func TestContainer(t *testing.T) {
|
|||||||
t.Cleanup(func() { container.SetOutput(oldOutput) })
|
t.Cleanup(func() { container.SetOutput(oldOutput) })
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("cancel", func(t *testing.T) {
|
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
|
||||||
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
|
|
||||||
|
|
||||||
c := helperNewContainer(ctx, "block")
|
|
||||||
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
|
||||||
c.WaitDelay = helperDefaultTimeout
|
|
||||||
|
|
||||||
ready := make(chan struct{})
|
|
||||||
if r, w, err := os.Pipe(); err != nil {
|
|
||||||
t.Fatalf("cannot pipe: %v", err)
|
|
||||||
} else {
|
|
||||||
c.ExtraFiles = append(c.ExtraFiles, w)
|
|
||||||
go func() {
|
|
||||||
defer close(ready)
|
|
||||||
if _, err = r.Read(make([]byte, 1)); err != nil {
|
|
||||||
panic(err.Error())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.Start(); err != nil {
|
|
||||||
hlog.PrintBaseError(err, "start:")
|
|
||||||
t.Fatalf("cannot start container: %v", err)
|
|
||||||
} else if err = c.Serve(); err != nil {
|
|
||||||
hlog.PrintBaseError(err, "serve:")
|
|
||||||
t.Errorf("cannot serve setup params: %v", err)
|
|
||||||
}
|
|
||||||
<-ready
|
|
||||||
cancel()
|
|
||||||
wantErr := context.Canceled
|
wantErr := context.Canceled
|
||||||
|
wantExitCode := 0
|
||||||
if err := c.Wait(); !errors.Is(err, wantErr) {
|
if err := c.Wait(); !errors.Is(err, wantErr) {
|
||||||
hlog.PrintBaseError(err, "wait:")
|
hlog.PrintBaseError(err, "wait:")
|
||||||
t.Fatalf("Wait: error = %v, want %v", err, wantErr)
|
t.Errorf("Wait: error = %v, want %v", err, wantErr)
|
||||||
}
|
}
|
||||||
})
|
if ps := c.ProcessState(); ps == nil {
|
||||||
|
t.Errorf("ProcessState unexpectedly returned nil")
|
||||||
|
} else if code := ps.ExitCode(); code != wantExitCode {
|
||||||
|
t.Errorf("ExitCode: %d, want %d", code, wantExitCode)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
t.Run("forward", testContainerCancel(func(c *container.Container) {
|
||||||
|
c.ForwardCancel = true
|
||||||
|
}, func(t *testing.T, c *container.Container) {
|
||||||
|
var exitError *exec.ExitError
|
||||||
|
if err := c.Wait(); !errors.As(err, &exitError) {
|
||||||
|
hlog.PrintBaseError(err, "wait:")
|
||||||
|
t.Errorf("Wait: error = %v", err)
|
||||||
|
}
|
||||||
|
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
|
||||||
|
t.Errorf("ExitCode: %d, want %d", code, blockExitCodeInterrupt)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
for i, tc := range containerTestCases {
|
for i, tc := range containerTestCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
@ -214,6 +207,46 @@ func hostnameFromTestCase(name string) string {
|
|||||||
return "test-" + strings.Join(strings.Fields(name), "-")
|
return "test-" + strings.Join(strings.Fields(name), "-")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testContainerCancel(
|
||||||
|
containerExtra func(c *container.Container),
|
||||||
|
waitCheck func(t *testing.T, c *container.Container),
|
||||||
|
) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
|
||||||
|
|
||||||
|
c := helperNewContainer(ctx, "block")
|
||||||
|
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
||||||
|
c.WaitDelay = helperDefaultTimeout
|
||||||
|
if containerExtra != nil {
|
||||||
|
containerExtra(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
ready := make(chan struct{})
|
||||||
|
if r, w, err := os.Pipe(); err != nil {
|
||||||
|
t.Fatalf("cannot pipe: %v", err)
|
||||||
|
} else {
|
||||||
|
c.ExtraFiles = append(c.ExtraFiles, w)
|
||||||
|
go func() {
|
||||||
|
defer close(ready)
|
||||||
|
if _, err = r.Read(make([]byte, 1)); err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Start(); err != nil {
|
||||||
|
hlog.PrintBaseError(err, "start:")
|
||||||
|
t.Fatalf("cannot start container: %v", err)
|
||||||
|
} else if err = c.Serve(); err != nil {
|
||||||
|
hlog.PrintBaseError(err, "serve:")
|
||||||
|
t.Errorf("cannot serve setup params: %v", err)
|
||||||
|
}
|
||||||
|
<-ready
|
||||||
|
cancel()
|
||||||
|
waitCheck(t, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestContainerString(t *testing.T) {
|
func TestContainerString(t *testing.T) {
|
||||||
c := container.New(t.Context(), "ldd", "/usr/bin/env")
|
c := container.New(t.Context(), "ldd", "/usr/bin/env")
|
||||||
c.SeccompFlags |= seccomp.AllowMultiarch
|
c.SeccompFlags |= seccomp.AllowMultiarch
|
||||||
@ -227,12 +260,21 @@ func TestContainerString(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
blockExitCodeInterrupt = 2
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
helperCommands = append(helperCommands, func(c command.Command) {
|
helperCommands = append(helperCommands, func(c command.Command) {
|
||||||
c.Command("block", command.UsageInternal, func(args []string) error {
|
c.Command("block", command.UsageInternal, func(args []string) error {
|
||||||
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
|
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
|
||||||
return fmt.Errorf("write to sync pipe: %v", err)
|
return fmt.Errorf("write to sync pipe: %v", err)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, os.Interrupt)
|
||||||
|
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
|
||||||
|
}
|
||||||
select {}
|
select {}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -277,7 +277,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
|||||||
msg.Suspend()
|
msg.Suspend()
|
||||||
|
|
||||||
if err := closeSetup(); err != nil {
|
if err := closeSetup(); err != nil {
|
||||||
log.Println("cannot close setup pipe:", err)
|
log.Printf("cannot close setup pipe: %v", err)
|
||||||
// not fatal
|
// not fatal
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -311,7 +311,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !errors.Is(err, ECHILD) {
|
if !errors.Is(err, ECHILD) {
|
||||||
log.Println("unexpected wait4 response:", err)
|
log.Printf("unexpected wait4 response: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
close(done)
|
close(done)
|
||||||
@ -319,7 +319,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
|||||||
|
|
||||||
// handle signals to dump withheld messages
|
// handle signals to dump withheld messages
|
||||||
sig := make(chan os.Signal, 2)
|
sig := make(chan os.Signal, 2)
|
||||||
signal.Notify(sig, SIGINT, SIGTERM)
|
signal.Notify(sig, os.Interrupt, CancelSignal)
|
||||||
|
|
||||||
// closed after residualProcessTimeout has elapsed after initial process death
|
// closed after residualProcessTimeout has elapsed after initial process death
|
||||||
timeout := make(chan struct{})
|
timeout := make(chan struct{})
|
||||||
@ -329,9 +329,16 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
|||||||
select {
|
select {
|
||||||
case s := <-sig:
|
case s := <-sig:
|
||||||
if msg.Resume() {
|
if msg.Resume() {
|
||||||
msg.Verbosef("terminating on %s after process start", s.String())
|
msg.Verbosef("%s after process start", s.String())
|
||||||
} else {
|
} else {
|
||||||
msg.Verbosef("terminating on %s", s.String())
|
msg.Verbosef("got %s", s.String())
|
||||||
|
}
|
||||||
|
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
|
||||||
|
msg.Verbose("forwarding context cancellation")
|
||||||
|
if err := cmd.Process.Signal(os.Interrupt); err != nil {
|
||||||
|
log.Printf("cannot forward cancellation: %v", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
case w := <-info:
|
case w := <-info:
|
||||||
@ -351,10 +358,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
|||||||
msg.Verbosef("initial process exited with status %#x", w.wstatus)
|
msg.Verbosef("initial process exited with status %#x", w.wstatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() { time.Sleep(residualProcessTimeout); close(timeout) }()
|
||||||
time.Sleep(residualProcessTimeout)
|
|
||||||
close(timeout)
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
case <-done:
|
case <-done:
|
||||||
msg.BeforeExit()
|
msg.BeforeExit()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user