diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-10-09 05:04:08 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-10-09 05:18:19 +0900 |
| commit | 87b5c30ef6d5b7b8a10d6a5a8610054c403923e6 (patch) | |
| tree | 805116c39a623bb30fc532af47ea3ed78b7304c0 /container | |
| parent | df9b77b077d967d35082adb8ab3b00dd67d7bc97 (diff) | |
message: relocate from container
This package is quite useful. This change allows it to be imported without importing container.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'container')
| -rw-r--r-- | container/autoroot.go | 3 | ||||
| -rw-r--r-- | container/autoroot_test.go | 3 | ||||
| -rw-r--r-- | container/container.go | 9 | ||||
| -rw-r--r-- | container/container_test.go | 11 | ||||
| -rw-r--r-- | container/dispatcher.go | 25 | ||||
| -rw-r--r-- | container/dispatcher_test.go | 15 | ||||
| -rw-r--r-- | container/executable.go | 6 | ||||
| -rw-r--r-- | container/executable_test.go | 3 | ||||
| -rw-r--r-- | container/init.go | 13 | ||||
| -rw-r--r-- | container/mount.go | 7 | ||||
| -rw-r--r-- | container/msg.go | 110 | ||||
| -rw-r--r-- | container/msg_test.go | 177 | ||||
| -rw-r--r-- | container/output.go | 77 | ||||
| -rw-r--r-- | container/output_test.go | 173 | ||||
| -rw-r--r-- | container/sysctl.go | 9 |
15 files changed, 58 insertions, 583 deletions
diff --git a/container/autoroot.go b/container/autoroot.go index 0ebe6b2b..f555acad 100644 --- a/container/autoroot.go +++ b/container/autoroot.go @@ -6,6 +6,7 @@ import ( "hakurei.app/container/check" "hakurei.app/container/fhs" + "hakurei.app/message" ) func init() { gob.Register(new(AutoRootOp)) } @@ -81,7 +82,7 @@ func (r *AutoRootOp) String() string { } // IsAutoRootBindable returns whether a dir entry name is selected for AutoRoot. -func IsAutoRootBindable(msg Msg, name string) bool { +func IsAutoRootBindable(msg message.Msg, name string) bool { switch name { case "proc", "dev", "tmp", "mnt", "etc": diff --git a/container/autoroot_test.go b/container/autoroot_test.go index 8c72b789..d53edc71 100644 --- a/container/autoroot_test.go +++ b/container/autoroot_test.go @@ -8,6 +8,7 @@ import ( "hakurei.app/container/bits" "hakurei.app/container/check" "hakurei.app/container/stub" + "hakurei.app/message" ) func TestAutoRootOp(t *testing.T) { @@ -195,7 +196,7 @@ func TestIsAutoRootBindable(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - var msg Msg + var msg message.Msg if tc.log { msg = &kstub{nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{ call("verbose", stub.ExpectArgs{[]any{"got unexpected root entry"}}, nil, nil), diff --git a/container/container.go b/container/container.go index 5b6514d4..ae83fc30 100644 --- a/container/container.go +++ b/container/container.go @@ -18,6 +18,7 @@ import ( "hakurei.app/container/check" "hakurei.app/container/fhs" "hakurei.app/container/seccomp" + "hakurei.app/message" ) const ( @@ -52,7 +53,7 @@ type ( cmd *exec.Cmd ctx context.Context - msg Msg + msg message.Msg Params } @@ -396,9 +397,9 @@ func (p *Container) ProcessState() *os.ProcessState { } // New returns the address to a new instance of [Container] that requires further initialisation before use. -func New(ctx context.Context, msg Msg) *Container { +func New(ctx context.Context, msg message.Msg) *Container { if msg == nil { - msg = NewMsg(nil) + msg = message.NewMsg(nil) } p := &Container{ctx: ctx, msg: msg, Params: Params{Ops: new(Ops)}} @@ -409,7 +410,7 @@ func New(ctx context.Context, msg Msg) *Container { } // NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields. -func NewCommand(ctx context.Context, msg Msg, pathname *check.Absolute, name string, args ...string) *Container { +func NewCommand(ctx context.Context, msg message.Msg, pathname *check.Absolute, name string, args ...string) *Container { z := New(ctx, msg) z.Path = pathname z.Args = append([]string{name}, args...) diff --git a/container/container_test.go b/container/container_test.go index 2d60f29d..6a85869f 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -26,6 +26,7 @@ import ( "hakurei.app/container/vfs" "hakurei.app/hst" "hakurei.app/ldd" + "hakurei.app/message" ) func TestStartError(t *testing.T) { @@ -152,13 +153,13 @@ func TestStartError(t *testing.T) { }) t.Run("msg", func(t *testing.T) { - if got, ok := container.GetErrorMessage(tc.err); !ok { + if got, ok := message.GetMessage(tc.err); !ok { if tc.msg != "" { - t.Errorf("GetErrorMessage: err does not implement MessageError") + t.Errorf("GetMessage: err does not implement MessageError") } return } else if got != tc.msg { - t.Errorf("GetErrorMessage: %q, want %q", got, tc.msg) + t.Errorf("GetMessage: %q, want %q", got, tc.msg) } }) }) @@ -545,7 +546,7 @@ func testContainerCancel( } func TestContainerString(t *testing.T) { - msg := container.NewMsg(nil) + msg := message.NewMsg(nil) c := container.NewCommand(t.Context(), msg, check.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env") c.SeccompFlags |= seccomp.AllowMultiarch c.SeccompRules = seccomp.Preset( @@ -711,7 +712,7 @@ func TestMain(m *testing.M) { } func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) { - msg := container.NewMsg(nil) + msg := message.NewMsg(nil) c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...) c.Env = append(c.Env, envDoCheck+"=1") c.Bind(check.MustAbs(os.Args[0]), absHelperInnerPath, 0) diff --git a/container/dispatcher.go b/container/dispatcher.go index 65881cc7..5f8c72cb 100644 --- a/container/dispatcher.go +++ b/container/dispatcher.go @@ -11,6 +11,7 @@ import ( "syscall" "hakurei.app/container/seccomp" + "hakurei.app/message" ) type osFile interface { @@ -37,7 +38,7 @@ type syscallDispatcher interface { setNoNewPrivs() error // lastcap provides [LastCap]. - lastcap(msg Msg) uintptr + lastcap(msg message.Msg) uintptr // capset provides capset. capset(hdrp *capHeader, datap *[2]capData) error // capBoundingSetDrop provides capBoundingSetDrop. @@ -52,9 +53,9 @@ type syscallDispatcher interface { receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) // bindMount provides procPaths.bindMount. - bindMount(msg Msg, source, target string, flags uintptr) error + bindMount(msg message.Msg, source, target string, flags uintptr) error // remount provides procPaths.remount. - remount(msg Msg, target string, flags uintptr) error + remount(msg message.Msg, target string, flags uintptr) error // mountTmpfs provides mountTmpfs. mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error // ensureFile provides ensureFile. @@ -122,11 +123,11 @@ type syscallDispatcher interface { wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) // printf provides the Printf method of [log.Logger]. - printf(msg Msg, format string, v ...any) + printf(msg message.Msg, format string, v ...any) // fatal provides the Fatal method of [log.Logger] - fatal(msg Msg, v ...any) + fatal(msg message.Msg, v ...any) // fatalf provides the Fatalf method of [log.Logger] - fatalf(msg Msg, format string, v ...any) + fatalf(msg message.Msg, format string, v ...any) } // direct implements syscallDispatcher on the current kernel. @@ -140,7 +141,7 @@ func (direct) setPtracer(pid uintptr) error { return SetPtracer(pid) } func (direct) setDumpable(dumpable uintptr) error { return SetDumpable(dumpable) } func (direct) setNoNewPrivs() error { return SetNoNewPrivs() } -func (direct) lastcap(msg Msg) uintptr { return LastCap(msg) } +func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) } func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) } func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) } func (direct) capAmbientClearAll() error { return capAmbientClearAll() } @@ -150,10 +151,10 @@ func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) { return Receive(key, e, fdp) } -func (direct) bindMount(msg Msg, source, target string, flags uintptr) error { +func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error { return hostProc.bindMount(msg, source, target, flags) } -func (direct) remount(msg Msg, target string, flags uintptr) error { +func (direct) remount(msg message.Msg, target string, flags uintptr) error { return hostProc.remount(msg, target, flags) } func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error { @@ -221,6 +222,6 @@ func (direct) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *s return syscall.Wait4(pid, wstatus, options, rusage) } -func (direct) printf(msg Msg, format string, v ...any) { msg.GetLogger().Printf(format, v...) } -func (direct) fatal(msg Msg, v ...any) { msg.GetLogger().Fatal(v...) } -func (direct) fatalf(msg Msg, format string, v ...any) { msg.GetLogger().Fatalf(format, v...) } +func (direct) printf(msg message.Msg, format string, v ...any) { msg.GetLogger().Printf(format, v...) } +func (direct) fatal(msg message.Msg, v ...any) { msg.GetLogger().Fatal(v...) } +func (direct) fatalf(msg message.Msg, format string, v ...any) { msg.GetLogger().Fatalf(format, v...) } diff --git a/container/dispatcher_test.go b/container/dispatcher_test.go index 2a2685fd..4e6ee76f 100644 --- a/container/dispatcher_test.go +++ b/container/dispatcher_test.go @@ -18,6 +18,7 @@ import ( "hakurei.app/container/seccomp" "hakurei.app/container/stub" + "hakurei.app/message" ) type opValidTestCase struct { @@ -329,7 +330,7 @@ func (k *kstub) setDumpable(dumpable uintptr) error { } func (k *kstub) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err } -func (k *kstub) lastcap(msg Msg) uintptr { +func (k *kstub) lastcap(msg message.Msg) uintptr { k.Helper() k.checkMsg(msg) return k.Expects("lastcap").Ret.(uintptr) @@ -409,7 +410,7 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error return } -func (k *kstub) bindMount(msg Msg, source, target string, flags uintptr) error { +func (k *kstub) bindMount(msg message.Msg, source, target string, flags uintptr) error { k.Helper() k.checkMsg(msg) return k.Expects("bindMount").Error( @@ -418,7 +419,7 @@ func (k *kstub) bindMount(msg Msg, source, target string, flags uintptr) error { stub.CheckArg(k.Stub, "flags", flags, 2)) } -func (k *kstub) remount(msg Msg, target string, flags uintptr) error { +func (k *kstub) remount(msg message.Msg, target string, flags uintptr) error { k.Helper() k.checkMsg(msg) return k.Expects("remount").Error( @@ -702,7 +703,7 @@ func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage return } -func (k *kstub) printf(_ Msg, format string, v ...any) { +func (k *kstub) printf(_ message.Msg, format string, v ...any) { k.Helper() if k.Expects("printf").Error( stub.CheckArg(k.Stub, "format", format, 0), @@ -711,7 +712,7 @@ func (k *kstub) printf(_ Msg, format string, v ...any) { } } -func (k *kstub) fatal(_ Msg, v ...any) { +func (k *kstub) fatal(_ message.Msg, v ...any) { k.Helper() if k.Expects("fatal").Error( stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil { @@ -720,7 +721,7 @@ func (k *kstub) fatal(_ Msg, v ...any) { panic(stub.PanicExit) } -func (k *kstub) fatalf(_ Msg, format string, v ...any) { +func (k *kstub) fatalf(_ message.Msg, format string, v ...any) { k.Helper() if k.Expects("fatalf").Error( stub.CheckArg(k.Stub, "format", format, 0), @@ -730,7 +731,7 @@ func (k *kstub) fatalf(_ Msg, format string, v ...any) { panic(stub.PanicExit) } -func (k *kstub) checkMsg(msg Msg) { +func (k *kstub) checkMsg(msg message.Msg) { k.Helper() var target *kstub diff --git a/container/executable.go b/container/executable.go index 9b6d4f66..f80a1f79 100644 --- a/container/executable.go +++ b/container/executable.go @@ -3,6 +3,8 @@ package container import ( "os" "sync" + + "hakurei.app/message" ) var ( @@ -10,7 +12,7 @@ var ( executableOnce sync.Once ) -func copyExecutable(msg Msg) { +func copyExecutable(msg message.Msg) { if name, err := os.Executable(); err != nil { msg.BeforeExit() msg.GetLogger().Fatalf("cannot read executable path: %v", err) @@ -19,7 +21,7 @@ func copyExecutable(msg Msg) { } } -func MustExecutable(msg Msg) string { +func MustExecutable(msg message.Msg) string { executableOnce.Do(func() { copyExecutable(msg) }) return executable } diff --git a/container/executable_test.go b/container/executable_test.go index 5608a8ae..33981bb6 100644 --- a/container/executable_test.go +++ b/container/executable_test.go @@ -5,11 +5,12 @@ import ( "testing" "hakurei.app/container" + "hakurei.app/message" ) func TestExecutable(t *testing.T) { for i := 0; i < 16; i++ { - if got := container.MustExecutable(container.NewMsg(nil)); got != os.Args[0] { + if got := container.MustExecutable(message.NewMsg(nil)); got != os.Args[0] { t.Errorf("MustExecutable: %q, want %q", got, os.Args[0]) } diff --git a/container/init.go b/container/init.go index 9481acd1..923a36e8 100644 --- a/container/init.go +++ b/container/init.go @@ -14,6 +14,7 @@ import ( "hakurei.app/container/fhs" "hakurei.app/container/seccomp" + "hakurei.app/message" ) const ( @@ -61,7 +62,7 @@ type ( setupState struct { nonrepeatable uintptr *Params - Msg + message.Msg } ) @@ -95,14 +96,14 @@ type initParams struct { } // Init is called by [TryArgv0] if the current process is the container init. -func Init(msg Msg) { +func Init(msg message.Msg) { if msg == nil { panic("attempting to call initEntrypoint with nil msg") } initEntrypoint(direct{}, msg) } -func initEntrypoint(k syscallDispatcher, msg Msg) { +func initEntrypoint(k syscallDispatcher, msg message.Msg) { k.lockOSThread() if k.getpid() != 1 { @@ -125,7 +126,7 @@ func initEntrypoint(k syscallDispatcher, msg Msg) { k.fatal(msg, "invalid setup descriptor") } if errors.Is(err, ErrReceiveEnv) { - k.fatal(msg, "HAKUREI_SETUP not set") + k.fatal(msg, setupEnv+" not set") } k.fatalf(msg, "cannot decode init setup payload: %v", err) @@ -448,11 +449,11 @@ const initName = "init" // TryArgv0 calls [Init] if the last element of argv0 is "init". // If a nil msg is passed, the system logger is used instead. -func TryArgv0(msg Msg) { +func TryArgv0(msg message.Msg) { if msg == nil { log.SetPrefix(initName + ": ") log.SetFlags(0) - msg = NewMsg(log.Default()) + msg = message.NewMsg(log.Default()) } if len(os.Args) > 0 && path.Base(os.Args[0]) == initName { diff --git a/container/mount.go b/container/mount.go index 0c459c9c..360a71dc 100644 --- a/container/mount.go +++ b/container/mount.go @@ -7,6 +7,7 @@ import ( . "syscall" "hakurei.app/container/vfs" + "hakurei.app/message" ) /* @@ -87,7 +88,7 @@ const ( ) // bindMount mounts source on target and recursively applies flags if MS_REC is set. -func (p *procPaths) bindMount(msg Msg, source, target string, flags uintptr) error { +func (p *procPaths) bindMount(msg message.Msg, source, target string, flags uintptr) error { // syscallDispatcher.bindMount and procPaths.remount must not be called from this function if err := p.k.mount(source, target, FstypeNULL, MS_SILENT|MS_BIND|flags&MS_REC, zeroString); err != nil { @@ -97,7 +98,7 @@ func (p *procPaths) bindMount(msg Msg, source, target string, flags uintptr) err } // remount applies flags on target, recursively if MS_REC is set. -func (p *procPaths) remount(msg Msg, target string, flags uintptr) error { +func (p *procPaths) remount(msg message.Msg, target string, flags uintptr) error { // syscallDispatcher methods bindMount, remount must not be called from this function var targetFinal string @@ -159,7 +160,7 @@ func (p *procPaths) remount(msg Msg, target string, flags uintptr) error { } // remountWithFlags remounts mount point described by [vfs.MountInfoNode]. -func remountWithFlags(k syscallDispatcher, msg Msg, n *vfs.MountInfoNode, mf uintptr) error { +func remountWithFlags(k syscallDispatcher, msg message.Msg, n *vfs.MountInfoNode, mf uintptr) error { // syscallDispatcher methods bindMount, remount must not be called from this function kf, unmatched := n.Flags() diff --git a/container/msg.go b/container/msg.go deleted file mode 100644 index 38c38ecd..00000000 --- a/container/msg.go +++ /dev/null @@ -1,110 +0,0 @@ -package container - -import ( - "errors" - "log" - "sync/atomic" -) - -// MessageError is an error with a user-facing message. -type MessageError interface { - // Message returns a user-facing error message. - Message() string - - error -} - -// GetErrorMessage returns whether an error implements [MessageError], and the message if it does. -func GetErrorMessage(err error) (string, bool) { - var e MessageError - if !errors.As(err, &e) || e == nil { - return zeroString, false - } - return e.Message(), true -} - -// Msg is used for package-wide verbose logging. -type Msg interface { - // GetLogger returns the address of the underlying [log.Logger]. - GetLogger() *log.Logger - - // IsVerbose atomically loads and returns whether [Msg] has verbose logging enabled. - IsVerbose() bool - // SwapVerbose atomically stores a new verbose state and returns the previous value held by [Msg]. - SwapVerbose(verbose bool) bool - // Verbose passes its argument to the Println method of the underlying [log.Logger] if IsVerbose returns true. - Verbose(v ...any) - // Verbosef passes its argument to the Printf method of the underlying [log.Logger] if IsVerbose returns true. - Verbosef(format string, v ...any) - - // Suspend causes the embedded [Suspendable] to withhold writes to its downstream [io.Writer]. - // Suspend returns false and is a noop if called between calls to Suspend and Resume. - Suspend() bool - - // Resume dumps the entire buffer held by the embedded [Suspendable] and stops withholding future writes. - // Resume returns false and is a noop if a call to Suspend does not precede it. - Resume() bool - - // BeforeExit runs implementation-specific cleanup code, and optionally prints warnings. - // BeforeExit is called before [os.Exit]. - BeforeExit() -} - -// defaultMsg is the default implementation of the [Msg] interface. -// The zero value is not safe for use. Callers should use the [NewMsg] function instead. -type defaultMsg struct { - verbose atomic.Bool - - logger *log.Logger - Suspendable -} - -// NewMsg initialises a downstream [log.Logger] for a new [Msg]. -// The [log.Logger] should no longer be configured after NewMsg returns. -// If downstream is nil, a new logger is initialised in its place. -func NewMsg(downstream *log.Logger) Msg { - if downstream == nil { - downstream = log.New(log.Writer(), "container: ", 0) - } - - m := defaultMsg{logger: downstream} - m.Suspendable.Downstream = downstream.Writer() - downstream.SetOutput(&m.Suspendable) - return &m -} - -func (msg *defaultMsg) GetLogger() *log.Logger { return msg.logger } - -func (msg *defaultMsg) IsVerbose() bool { return msg.verbose.Load() } -func (msg *defaultMsg) SwapVerbose(verbose bool) bool { return msg.verbose.Swap(verbose) } -func (msg *defaultMsg) Verbose(v ...any) { - if msg.verbose.Load() { - msg.logger.Println(v...) - } -} -func (msg *defaultMsg) Verbosef(format string, v ...any) { - if msg.verbose.Load() { - msg.logger.Printf(format, v...) - } -} - -// Resume calls [Suspendable.Resume] and prints a message if buffer was filled -// between calls to [Suspendable.Suspend] and Resume. -func (msg *defaultMsg) Resume() bool { - resumed, dropped, _, err := msg.Suspendable.Resume() - if err != nil { - // probably going to result in an error as well, so this message is as good as unreachable - msg.logger.Printf("cannot dump buffer on resume: %v", err) - } - if resumed && dropped > 0 { - msg.logger.Printf("dropped %d bytes while output is suspended", dropped) - } - return resumed -} - -// BeforeExit prints a message if called between calls to [Suspendable.Suspend] and Resume. -func (msg *defaultMsg) BeforeExit() { - if msg.Resume() { - msg.logger.Printf("beforeExit reached on suspended output") - } -} diff --git a/container/msg_test.go b/container/msg_test.go deleted file mode 100644 index ed5c0c59..00000000 --- a/container/msg_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package container_test - -import ( - "bytes" - "errors" - "io" - "log" - "strings" - "syscall" - "testing" - - "hakurei.app/container" - "hakurei.app/container/stub" -) - -func TestMessageError(t *testing.T) { - testCases := []struct { - name string - err error - want string - wantOk bool - }{ - {"nil", nil, "", false}, - {"new", errors.New(":3"), "", false}, - {"start", &container.StartError{ - Step: "meow", - Err: syscall.ENOTRECOVERABLE, - }, "cannot meow: state not recoverable", true}, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got, ok := container.GetErrorMessage(tc.err) - if got != tc.want { - t.Errorf("GetErrorMessage: %q, want %q", got, tc.want) - } - if ok != tc.wantOk { - t.Errorf("GetErrorMessage: ok = %v, want %v", ok, tc.wantOk) - } - }) - } -} - -func TestDefaultMsg(t *testing.T) { - // copied from output.go - const suspendBufMax = 1 << 24 - - t.Run("logger", func(t *testing.T) { - t.Run("nil", func(t *testing.T) { - got := container.NewMsg(nil).GetLogger() - - if out := got.Writer().(*container.Suspendable).Downstream; out != log.Writer() { - t.Errorf("GetLogger: Downstream = %#v", out) - } - - if prefix := got.Prefix(); prefix != "container: " { - t.Errorf("GetLogger: prefix = %q", prefix) - } - }) - - t.Run("takeover", func(t *testing.T) { - l := log.New(io.Discard, "\x00", 0xdeadbeef) - got := container.NewMsg(l) - - if logger := got.GetLogger(); logger != l { - t.Errorf("GetLogger: %#v, want %#v", logger, l) - } - - if ds := l.Writer().(*container.Suspendable).Downstream; ds != io.Discard { - t.Errorf("GetLogger: Downstream = %#v", ds) - } - }) - }) - - dw := expectWriter{t: t} - - steps := []struct { - name string - pt, next []byte - err error - - f func(t *testing.T, msg container.Msg) - }{ - {"zero verbose", nil, nil, nil, func(t *testing.T, msg container.Msg) { - if msg.IsVerbose() { - t.Error("IsVerbose unexpected true") - } - }}, - - {"swap false", nil, nil, nil, func(t *testing.T, msg container.Msg) { - if msg.SwapVerbose(false) { - t.Error("SwapVerbose unexpected true") - } - }}, - {"write discard", nil, nil, nil, func(_ *testing.T, msg container.Msg) { - msg.Verbose("\x00") - msg.Verbosef("\x00") - }}, - {"verbose false", nil, nil, nil, func(t *testing.T, msg container.Msg) { - if msg.IsVerbose() { - t.Error("IsVerbose unexpected true") - } - }}, - - {"swap true", nil, nil, nil, func(t *testing.T, msg container.Msg) { - if msg.SwapVerbose(true) { - t.Error("SwapVerbose unexpected true") - } - }}, - {"write verbose", []byte("test: \x00\n"), nil, nil, func(_ *testing.T, msg container.Msg) { - msg.Verbose("\x00") - }}, - {"write verbosef", []byte(`test: "\x00"` + "\n"), nil, nil, func(_ *testing.T, msg container.Msg) { - msg.Verbosef("%q", "\x00") - }}, - {"verbose true", nil, nil, nil, func(t *testing.T, msg container.Msg) { - if !msg.IsVerbose() { - t.Error("IsVerbose unexpected false") - } - }}, - - {"resume noop", nil, nil, nil, func(t *testing.T, msg container.Msg) { - if msg.Resume() { - t.Error("Resume unexpected success") - } - }}, - {"beforeExit noop", nil, nil, nil, func(_ *testing.T, msg container.Msg) { - msg.BeforeExit() - }}, - - {"beforeExit suspend", nil, nil, nil, func(_ *testing.T, msg container.Msg) { - msg.Suspend() - }}, - {"beforeExit message", []byte("test: beforeExit reached on suspended output\n"), nil, nil, func(_ *testing.T, msg container.Msg) { - msg.BeforeExit() - }}, - {"post beforeExit resume noop", nil, nil, nil, func(t *testing.T, msg container.Msg) { - if msg.Resume() { - t.Error("Resume unexpected success") - } - }}, - - {"suspend", nil, nil, nil, func(_ *testing.T, msg container.Msg) { - msg.Suspend() - }}, - {"suspend write", nil, nil, nil, func(_ *testing.T, msg container.Msg) { - msg.GetLogger().Print("\x00") - }}, - {"resume error", []byte("test: \x00\n"), []byte("test: cannot dump buffer on resume: unique error 0 injected by the test suite\n"), stub.UniqueError(0), func(t *testing.T, msg container.Msg) { - if !msg.Resume() { - t.Error("Resume unexpected failure") - } - }}, - - {"suspend drop", nil, nil, nil, func(_ *testing.T, msg container.Msg) { - msg.Suspend() - }}, - {"suspend write fill", nil, nil, nil, func(_ *testing.T, msg container.Msg) { - msg.GetLogger().Print(strings.Repeat("\x00", suspendBufMax)) - }}, - {"resume dropped", append([]byte("test: "), bytes.Repeat([]byte{0}, suspendBufMax-6)...), []byte("test: dropped 7 bytes while output is suspended\n"), nil, func(t *testing.T, msg container.Msg) { - if !msg.Resume() { - t.Error("Resume unexpected failure") - } - }}, - } - - msg := container.NewMsg(log.New(&dw, "test: ", 0)) - for _, step := range steps { - // these share the same writer, so cannot be subtests - t.Logf("running step %q", step.name) - dw.expect, dw.next, dw.err = step.pt, step.next, step.err - step.f(t, msg) - if dw.expect != nil { - t.Errorf("expect: %q", string(dw.expect)) - } - } -} diff --git a/container/output.go b/container/output.go deleted file mode 100644 index 4d799eb4..00000000 --- a/container/output.go +++ /dev/null @@ -1,77 +0,0 @@ -package container - -import ( - "bytes" - "io" - "sync" - "sync/atomic" - "syscall" -) - -const ( - suspendBufInitial = 1 << 12 - suspendBufMax = 1 << 24 -) - -// Suspendable proxies writes to a downstream [io.Writer] but optionally withholds writes -// between calls to Suspend and Resume. -type Suspendable struct { - Downstream io.Writer - - s atomic.Bool - - buf bytes.Buffer - // for growing buf - bufOnce sync.Once - // for synchronising all other buf operations - bufMu sync.Mutex - - dropped int -} - -func (s *Suspendable) Write(p []byte) (n int, err error) { - if !s.s.Load() { - return s.Downstream.Write(p) - } - s.bufOnce.Do(func() { s.buf.Grow(suspendBufInitial) }) - - s.bufMu.Lock() - defer s.bufMu.Unlock() - - if free := suspendBufMax - s.buf.Len(); free < len(p) { - // fast path - if free <= 0 { - s.dropped += len(p) - return 0, syscall.ENOMEM - } - - n, _ = s.buf.Write(p[:free]) - err = syscall.ENOMEM - s.dropped += len(p) - n - return - } - - return s.buf.Write(p) -} - -// IsSuspended returns whether [Suspendable] is currently between a call to Suspend and Resume. -func (s *Suspendable) IsSuspended() bool { return s.s.Load() } - -// Suspend causes [Suspendable] to start withholding output in its buffer. -func (s *Suspendable) Suspend() bool { return s.s.CompareAndSwap(false, true) } - -// Resume undoes the effect of Suspend and dumps the buffered into the downstream [io.Writer]. -func (s *Suspendable) Resume() (resumed bool, dropped uintptr, n int64, err error) { - if s.s.CompareAndSwap(true, false) { - s.bufMu.Lock() - defer s.bufMu.Unlock() - - resumed = true - dropped = uintptr(s.dropped) - - s.dropped = 0 - n, err = io.Copy(s.Downstream, &s.buf) - s.buf.Reset() - } - return -} diff --git a/container/output_test.go b/container/output_test.go deleted file mode 100644 index b6dedf16..00000000 --- a/container/output_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package container_test - -import ( - "bytes" - "errors" - "reflect" - "strconv" - "syscall" - "testing" - - "hakurei.app/container" - "hakurei.app/container/stub" -) - -func TestSuspendable(t *testing.T) { - // copied from output.go - const suspendBufMax = 1 << 24 - - const ( - // equivalent to len(want.pt) - nSpecialPtEquiv = -iota - 1 - // equivalent to len(want.w) - nSpecialWEquiv - // suspends writer before executing test case, implies nSpecialWEquiv - nSpecialSuspend - // offset: resume writer and measure against dump instead, implies nSpecialPtEquiv - nSpecialDump - ) - - // shares the same writer - steps := []struct { - name string - w, pt []byte - err error - wantErr error - n int - }{ - {"simple", []byte{0xde, 0xad, 0xbe, 0xef}, []byte{0xde, 0xad, 0xbe, 0xef}, - nil, nil, nSpecialPtEquiv}, - - {"error", []byte{0xb, 0xad}, []byte{0xb, 0xad}, - stub.UniqueError(0), stub.UniqueError(0), nSpecialPtEquiv}, - - {"suspend short", []byte{0}, nil, - nil, nil, nSpecialSuspend}, - {"sw short 0", []byte{0xca, 0xfe, 0xba, 0xbe}, nil, - nil, nil, nSpecialWEquiv}, - {"sw short 1", []byte{0xff}, nil, - nil, nil, nSpecialWEquiv}, - {"resume short", nil, []byte{0, 0xca, 0xfe, 0xba, 0xbe, 0xff}, nil, nil, - nSpecialDump}, - - {"long pt", bytes.Repeat([]byte{0xff}, suspendBufMax+1), bytes.Repeat([]byte{0xff}, suspendBufMax+1), - nil, nil, nSpecialPtEquiv}, - - {"suspend fill", bytes.Repeat([]byte{0xfe}, suspendBufMax), nil, - nil, nil, nSpecialSuspend}, - {"drop", []byte{0}, nil, - nil, syscall.ENOMEM, 0}, - {"drop error", []byte{0}, nil, - stub.UniqueError(1), syscall.ENOMEM, 0}, - {"resume fill", nil, bytes.Repeat([]byte{0xfe}, suspendBufMax), - nil, nil, nSpecialDump - 2}, - - {"suspend fill partial", bytes.Repeat([]byte{0xfd}, suspendBufMax-0xf), nil, - nil, nil, nSpecialSuspend}, - {"partial write", bytes.Repeat([]byte{0xad}, 0x1f), nil, - nil, syscall.ENOMEM, 0xf}, - {"full drop", []byte{0}, nil, - nil, syscall.ENOMEM, 0}, - {"resume fill partial", nil, append(bytes.Repeat([]byte{0xfd}, suspendBufMax-0xf), bytes.Repeat([]byte{0xad}, 0xf)...), - nil, nil, nSpecialDump - 0x10 - 1}, - } - - var dw expectWriter - - w := container.Suspendable{Downstream: &dw} - for _, step := range steps { - // these share the same writer, so cannot be subtests - t.Logf("writing step %q", step.name) - dw.expect, dw.err = step.pt, step.err - - var ( - gotN int - gotErr error - ) - - wantN := step.n - switch wantN { - case nSpecialPtEquiv: - wantN = len(step.pt) - gotN, gotErr = w.Write(step.w) - - case nSpecialWEquiv: - wantN = len(step.w) - gotN, gotErr = w.Write(step.w) - - case nSpecialSuspend: - s := w.IsSuspended() - if ok := w.Suspend(); s && ok { - t.Fatal("Suspend: unexpected success") - } - - wantN = len(step.w) - gotN, gotErr = w.Write(step.w) - - default: - if wantN <= nSpecialDump { - if !w.IsSuspended() { - t.Fatal("IsSuspended unexpected false") - } - - resumed, dropped, n, err := w.Resume() - if !resumed { - t.Fatal("Resume: resumed = false") - } - if wantDropped := nSpecialDump - wantN; int(dropped) != wantDropped { - t.Errorf("Resume: dropped = %d, want %d", dropped, wantDropped) - } - - wantN = len(step.pt) - gotN, gotErr = int(n), err - } else { - gotN, gotErr = w.Write(step.w) - } - } - - if gotN != wantN { - t.Errorf("Write: n = %d, want %d", gotN, wantN) - } - - if !reflect.DeepEqual(gotErr, step.wantErr) { - t.Errorf("Write: %v", gotErr) - } - - if dw.expect != nil { - t.Errorf("expect: %q", string(dw.expect)) - } - } -} - -// expectWriter compares Write calls to expect. -type expectWriter struct { - expect []byte - err error - - // optional consecutive write - next []byte - - // optional, calls Error on failure if not nil - t *testing.T -} - -func (w *expectWriter) Write(p []byte) (n int, err error) { - defer func() { w.expect = w.next; w.next = nil }() - - n, err = len(p), w.err - if w.expect == nil { - n, err = 0, errors.New("unexpected call to Write: "+strconv.Quote(string(p))) - if w.t != nil { - w.t.Error(err.Error()) - } - return - } - if string(p) != string(w.expect) { - n, err = 0, errors.New("p = "+strconv.Quote(string(p))+", want "+strconv.Quote(string(w.expect))) - if w.t != nil { - w.t.Error(err.Error()) - } - return - } - return -} diff --git a/container/sysctl.go b/container/sysctl.go index 9a1e818e..4387c4da 100644 --- a/container/sysctl.go +++ b/container/sysctl.go @@ -7,6 +7,7 @@ import ( "sync" "hakurei.app/container/fhs" + "hakurei.app/message" ) var ( @@ -23,7 +24,7 @@ const ( kernelCapLastCapPath = fhs.ProcSys + "kernel/cap_last_cap" ) -func mustReadSysctl(msg Msg) { +func mustReadSysctl(msg message.Msg) { sysctlOnce.Do(func() { if v, err := os.ReadFile(kernelOverflowuidPath); err != nil { msg.GetLogger().Fatalf("cannot read %q: %v", kernelOverflowuidPath, err) @@ -45,6 +46,6 @@ func mustReadSysctl(msg Msg) { }) } -func OverflowUid(msg Msg) int { mustReadSysctl(msg); return kernelOverflowuid } -func OverflowGid(msg Msg) int { mustReadSysctl(msg); return kernelOverflowgid } -func LastCap(msg Msg) uintptr { mustReadSysctl(msg); return uintptr(kernelCapLastCap) } +func OverflowUid(msg message.Msg) int { mustReadSysctl(msg); return kernelOverflowuid } +func OverflowGid(msg message.Msg) int { mustReadSysctl(msg); return kernelOverflowgid } +func LastCap(msg message.Msg) uintptr { mustReadSysctl(msg); return uintptr(kernelCapLastCap) } |
