From 87b5c30ef6d5b7b8a10d6a5a8610054c403923e6 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Thu, 9 Oct 2025 05:04:08 +0900 Subject: message: relocate from container This package is quite useful. This change allows it to be imported without importing container. Signed-off-by: Ophestra --- cmd/hakurei/command.go | 4 +- cmd/hakurei/command_test.go | 4 +- cmd/hakurei/main.go | 3 +- cmd/hakurei/parse.go | 8 +- cmd/hakurei/print.go | 4 +- cmd/hpkg/main.go | 4 +- cmd/hpkg/paths.go | 4 +- cmd/hpkg/proc.go | 4 +- cmd/hpkg/with.go | 8 +- container/autoroot.go | 3 +- container/autoroot_test.go | 3 +- container/container.go | 9 +- container/container_test.go | 11 +-- container/dispatcher.go | 25 +++--- container/dispatcher_test.go | 15 ++-- container/executable.go | 6 +- container/executable_test.go | 3 +- container/init.go | 13 +-- container/mount.go | 7 +- container/msg.go | 110 ------------------------ container/msg_test.go | 177 -------------------------------------- container/output.go | 77 ----------------- container/output_test.go | 173 ------------------------------------- container/sysctl.go | 9 +- helper/container.go | 3 +- hst/dbus_test.go | 8 +- hst/hst_test.go | 8 +- internal/app/app.go | 4 +- internal/app/app_test.go | 7 +- internal/app/dispatcher.go | 9 +- internal/app/dispatcher_test.go | 6 +- internal/app/finalise.go | 4 +- internal/app/hsu.go | 6 +- internal/app/outcome.go | 7 +- internal/app/process.go | 9 +- internal/app/shim.go | 7 +- internal/app/spcontainer.go | 3 +- internal/app/state/multi.go | 6 +- internal/app/state/multi_test.go | 4 +- ldd/exec.go | 3 +- message/message.go | 111 ++++++++++++++++++++++++ message/message_test.go | 178 +++++++++++++++++++++++++++++++++++++++ message/output.go | 77 +++++++++++++++++ message/output_test.go | 173 +++++++++++++++++++++++++++++++++++++ system/dbus/dbus_test.go | 10 +-- system/dbus/export_test.go | 4 +- system/dbus/proxy.go | 6 +- system/output.go | 5 +- system/output_test.go | 7 +- system/system.go | 6 +- system/system_test.go | 34 ++++---- 51 files changed, 707 insertions(+), 682 deletions(-) delete mode 100644 container/msg.go delete mode 100644 container/msg_test.go delete mode 100644 container/output.go delete mode 100644 container/output_test.go create mode 100644 message/message.go create mode 100644 message/message_test.go create mode 100644 message/output.go create mode 100644 message/output_test.go diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go index e3356470..213cc416 100644 --- a/cmd/hakurei/command.go +++ b/cmd/hakurei/command.go @@ -15,17 +15,17 @@ import ( "time" "hakurei.app/command" - "hakurei.app/container" "hakurei.app/container/check" "hakurei.app/container/fhs" "hakurei.app/hst" "hakurei.app/internal" "hakurei.app/internal/app" "hakurei.app/internal/app/state" + "hakurei.app/message" "hakurei.app/system/dbus" ) -func buildCommand(ctx context.Context, msg container.Msg, early *earlyHardeningErrs, out io.Writer) command.Command { +func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command { var ( flagVerbose bool flagJSON bool diff --git a/cmd/hakurei/command_test.go b/cmd/hakurei/command_test.go index d22249f3..d29485de 100644 --- a/cmd/hakurei/command_test.go +++ b/cmd/hakurei/command_test.go @@ -7,7 +7,7 @@ import ( "testing" "hakurei.app/command" - "hakurei.app/container" + "hakurei.app/message" ) func TestHelp(t *testing.T) { @@ -69,7 +69,7 @@ Flags: for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { out := new(bytes.Buffer) - c := buildCommand(t.Context(), container.NewMsg(nil), new(earlyHardeningErrs), out) + c := buildCommand(t.Context(), message.NewMsg(nil), new(earlyHardeningErrs), out) if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) { t.Errorf("Parse: error = %v; want %v", err, command.ErrHelp) diff --git a/cmd/hakurei/main.go b/cmd/hakurei/main.go index 5cc8224d..dcea40c0 100644 --- a/cmd/hakurei/main.go +++ b/cmd/hakurei/main.go @@ -13,6 +13,7 @@ import ( "syscall" "hakurei.app/container" + "hakurei.app/message" ) var ( @@ -31,7 +32,7 @@ func main() { log.SetPrefix("hakurei: ") log.SetFlags(0) - msg := container.NewMsg(log.Default()) + msg := message.NewMsg(log.Default()) early := earlyHardeningErrs{ yamaLSM: container.SetPtracer(0), diff --git a/cmd/hakurei/parse.go b/cmd/hakurei/parse.go index 14b9c167..3d15f665 100644 --- a/cmd/hakurei/parse.go +++ b/cmd/hakurei/parse.go @@ -10,13 +10,13 @@ import ( "strings" "syscall" - "hakurei.app/container" "hakurei.app/hst" "hakurei.app/internal/app" "hakurei.app/internal/app/state" + "hakurei.app/message" ) -func tryPath(msg container.Msg, name string) (config *hst.Config) { +func tryPath(msg message.Msg, name string) (config *hst.Config) { var r io.Reader config = new(hst.Config) @@ -49,7 +49,7 @@ func tryPath(msg container.Msg, name string) (config *hst.Config) { return } -func tryFd(msg container.Msg, name string) io.ReadCloser { +func tryFd(msg message.Msg, name string) io.ReadCloser { if v, err := strconv.Atoi(name); err != nil { if !errors.Is(err, strconv.ErrSyntax) { msg.Verbosef("name cannot be interpreted as int64: %v", err) @@ -68,7 +68,7 @@ func tryFd(msg container.Msg, name string) io.ReadCloser { } } -func tryShort(msg container.Msg, name string) (config *hst.Config, entry *state.State) { +func tryShort(msg message.Msg, name string) (config *hst.Config, entry *state.State) { likePrefix := false if len(name) <= 32 { likePrefix = true diff --git a/cmd/hakurei/print.go b/cmd/hakurei/print.go index 1a97b858..e013909d 100644 --- a/cmd/hakurei/print.go +++ b/cmd/hakurei/print.go @@ -11,10 +11,10 @@ import ( "text/tabwriter" "time" - "hakurei.app/container" "hakurei.app/hst" "hakurei.app/internal/app" "hakurei.app/internal/app/state" + "hakurei.app/message" ) func printShowSystem(output io.Writer, short, flagJSON bool) { @@ -56,7 +56,7 @@ func printShowInstance( if err := config.Validate(); err != nil { valid = false - if m, ok := container.GetErrorMessage(err); ok { + if m, ok := message.GetMessage(err); ok { mustPrint(output, "Error: "+m+"!\n\n") } } diff --git a/cmd/hpkg/main.go b/cmd/hpkg/main.go index bb3f8865..e3346bff 100644 --- a/cmd/hpkg/main.go +++ b/cmd/hpkg/main.go @@ -11,10 +11,10 @@ import ( "syscall" "hakurei.app/command" - "hakurei.app/container" "hakurei.app/container/check" "hakurei.app/container/fhs" "hakurei.app/hst" + "hakurei.app/message" ) var ( @@ -24,7 +24,7 @@ var ( func main() { log.SetPrefix("hpkg: ") log.SetFlags(0) - msg := container.NewMsg(log.Default()) + msg := message.NewMsg(log.Default()) if err := os.Setenv("SHELL", pathShell.String()); err != nil { log.Fatalf("cannot set $SHELL: %v", err) diff --git a/cmd/hpkg/paths.go b/cmd/hpkg/paths.go index e5c045a5..811c7b7a 100644 --- a/cmd/hpkg/paths.go +++ b/cmd/hpkg/paths.go @@ -7,10 +7,10 @@ import ( "strconv" "sync/atomic" - "hakurei.app/container" "hakurei.app/container/check" "hakurei.app/container/fhs" "hakurei.app/hst" + "hakurei.app/message" ) const bash = "bash" @@ -52,7 +52,7 @@ func lookPath(file string) string { var beforeRunFail = new(atomic.Pointer[func()]) -func mustRun(msg container.Msg, name string, arg ...string) { +func mustRun(msg message.Msg, name string, arg ...string) { msg.Verbosef("spawning process: %q %q", name, arg) cmd := exec.Command(name, arg...) cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr diff --git a/cmd/hpkg/proc.go b/cmd/hpkg/proc.go index 1bbf5b49..70889106 100644 --- a/cmd/hpkg/proc.go +++ b/cmd/hpkg/proc.go @@ -9,14 +9,14 @@ import ( "os" "os/exec" - "hakurei.app/container" "hakurei.app/hst" "hakurei.app/internal" + "hakurei.app/message" ) var hakureiPath = internal.MustHakureiPath() -func mustRunApp(ctx context.Context, msg container.Msg, config *hst.Config, beforeFail func()) { +func mustRunApp(ctx context.Context, msg message.Msg, config *hst.Config, beforeFail func()) { var ( cmd *exec.Cmd st io.WriteCloser diff --git a/cmd/hpkg/with.go b/cmd/hpkg/with.go index df03a21d..cb86f502 100644 --- a/cmd/hpkg/with.go +++ b/cmd/hpkg/with.go @@ -5,15 +5,15 @@ import ( "os" "strings" - "hakurei.app/container" "hakurei.app/container/check" "hakurei.app/container/fhs" "hakurei.app/hst" + "hakurei.app/message" ) func withNixDaemon( ctx context.Context, - msg container.Msg, + msg message.Msg, action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config, app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(), ) { @@ -64,7 +64,7 @@ func withNixDaemon( func withCacheDir( ctx context.Context, - msg container.Msg, + msg message.Msg, action string, command []string, workDir *check.Absolute, app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) { mustRunAppDropShell(ctx, msg, &hst.Config{ @@ -102,7 +102,7 @@ func withCacheDir( }, dropShell, beforeFail) } -func mustRunAppDropShell(ctx context.Context, msg container.Msg, config *hst.Config, dropShell bool, beforeFail func()) { +func mustRunAppDropShell(ctx context.Context, msg message.Msg, config *hst.Config, dropShell bool, beforeFail func()) { if dropShell { if config.Container != nil { config.Container.Args = []string{bash, "-l"} 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) } diff --git a/helper/container.go b/helper/container.go index 47080383..b85f5859 100644 --- a/helper/container.go +++ b/helper/container.go @@ -12,12 +12,13 @@ import ( "hakurei.app/container" "hakurei.app/container/check" "hakurei.app/helper/proc" + "hakurei.app/message" ) // New initialises a Helper instance with wt as the null-terminated argument writer. func New( ctx context.Context, - msg container.Msg, + msg message.Msg, pathname *check.Absolute, name string, wt io.WriterTo, stat bool, diff --git a/hst/dbus_test.go b/hst/dbus_test.go index 8b04494b..d575f7d4 100644 --- a/hst/dbus_test.go +++ b/hst/dbus_test.go @@ -5,8 +5,8 @@ import ( "slices" "testing" - "hakurei.app/container" "hakurei.app/hst" + "hakurei.app/message" ) func TestBadInterfaceError(t *testing.T) { @@ -26,10 +26,10 @@ func TestBadInterfaceError(t *testing.T) { if gotError := tc.err.Error(); gotError != tc.want { t.Errorf("Error: %s, want %s", gotError, tc.want) } - if gotMessage, ok := container.GetErrorMessage(tc.err); !ok { - t.Error("GetErrorMessage: ok = false") + if gotMessage, ok := message.GetMessage(tc.err); !ok { + t.Error("GetMessage: ok = false") } else if gotMessage != tc.want { - t.Errorf("GetErrorMessage: %s, want %s", gotMessage, tc.want) + t.Errorf("GetMessage: %s, want %s", gotMessage, tc.want) } }) } diff --git a/hst/hst_test.go b/hst/hst_test.go index 90380e07..0bd6c2b0 100644 --- a/hst/hst_test.go +++ b/hst/hst_test.go @@ -8,9 +8,9 @@ import ( "syscall" "testing" - "hakurei.app/container" "hakurei.app/container/stub" "hakurei.app/hst" + "hakurei.app/message" ) func TestAppError(t *testing.T) { @@ -65,14 +65,14 @@ func TestAppError(t *testing.T) { }) t.Run("message", func(t *testing.T) { - gotMessage, gotMessageOk := container.GetErrorMessage(tc.err) + gotMessage, gotMessageOk := message.GetMessage(tc.err) if want := tc.message != "\x00"; gotMessageOk != want { - t.Errorf("GetErrorMessage: ok = %v, want %v", gotMessage, want) + t.Errorf("GetMessage: ok = %v, want %v", gotMessage, want) } if gotMessageOk { if gotMessage != tc.message { - t.Errorf("GetErrorMessage: %s, want %s", gotMessage, tc.message) + t.Errorf("GetMessage: %s, want %s", gotMessage, tc.message) } } }) diff --git a/internal/app/app.go b/internal/app/app.go index 102c273a..0d5f1a50 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -6,13 +6,13 @@ import ( "log" "os" - "hakurei.app/container" "hakurei.app/hst" "hakurei.app/internal/app/state" + "hakurei.app/message" ) // Main runs an app according to [hst.Config] and terminates. Main does not return. -func Main(ctx context.Context, msg container.Msg, config *hst.Config) { +func Main(ctx context.Context, msg message.Msg, config *hst.Config) { var id state.ID if err := state.NewAppID(&id); err != nil { log.Fatal(err) diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 9cab3581..7dcbc7d0 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -23,12 +23,13 @@ import ( "hakurei.app/container/fhs" "hakurei.app/hst" "hakurei.app/internal/app/state" + "hakurei.app/message" "hakurei.app/system" "hakurei.app/system/acl" ) func TestApp(t *testing.T) { - msg := container.NewMsg(nil) + msg := message.NewMsg(nil) msg.SwapVerbose(testing.Verbose()) testCases := []struct { @@ -744,8 +745,8 @@ func (k *stubNixOS) cmdOutput(cmd *exec.Cmd) ([]byte, error) { } } -func (k *stubNixOS) overflowUid(container.Msg) int { return 65534 } -func (k *stubNixOS) overflowGid(container.Msg) int { return 65534 } +func (k *stubNixOS) overflowUid(message.Msg) int { return 65534 } +func (k *stubNixOS) overflowGid(message.Msg) int { return 65534 } func (k *stubNixOS) mustHsuPath() *check.Absolute { return m("/proc/nonexistent/hsu") } diff --git a/internal/app/dispatcher.go b/internal/app/dispatcher.go index f3759023..6e00a9b7 100644 --- a/internal/app/dispatcher.go +++ b/internal/app/dispatcher.go @@ -12,6 +12,7 @@ import ( "hakurei.app/container" "hakurei.app/container/check" "hakurei.app/internal" + "hakurei.app/message" ) // osFile represents [os.File]. @@ -53,9 +54,9 @@ type syscallDispatcher interface { cmdOutput(cmd *exec.Cmd) ([]byte, error) // overflowUid provides [container.OverflowUid]. - overflowUid(msg container.Msg) int + overflowUid(msg message.Msg) int // overflowGid provides [container.OverflowGid]. - overflowGid(msg container.Msg) int + overflowGid(msg message.Msg) int // mustHsuPath provides [internal.MustHsuPath]. mustHsuPath() *check.Absolute @@ -90,8 +91,8 @@ func (direct) lookupGroupId(name string) (gid string, err error) { func (direct) cmdOutput(cmd *exec.Cmd) ([]byte, error) { return cmd.Output() } -func (direct) overflowUid(msg container.Msg) int { return container.OverflowUid(msg) } -func (direct) overflowGid(msg container.Msg) int { return container.OverflowGid(msg) } +func (direct) overflowUid(msg message.Msg) int { return container.OverflowUid(msg) } +func (direct) overflowGid(msg message.Msg) int { return container.OverflowGid(msg) } func (direct) mustHsuPath() *check.Absolute { return internal.MustHsuPath() } diff --git a/internal/app/dispatcher_test.go b/internal/app/dispatcher_test.go index 834b11b6..f6d10baa 100644 --- a/internal/app/dispatcher_test.go +++ b/internal/app/dispatcher_test.go @@ -4,8 +4,8 @@ import ( "os" "os/exec" - "hakurei.app/container" "hakurei.app/container/check" + "hakurei.app/message" ) type panicDispatcher struct{} @@ -21,7 +21,7 @@ func (panicDispatcher) tempdir() string { panic("unreachab func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") } func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") } func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") } -func (panicDispatcher) overflowUid(container.Msg) int { panic("unreachable") } -func (panicDispatcher) overflowGid(container.Msg) int { panic("unreachable") } +func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") } +func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") } func (panicDispatcher) mustHsuPath() *check.Absolute { panic("unreachable") } func (panicDispatcher) fatalf(string, ...any) { panic("unreachable") } diff --git a/internal/app/finalise.go b/internal/app/finalise.go index a2a8f30e..7693f253 100644 --- a/internal/app/finalise.go +++ b/internal/app/finalise.go @@ -8,9 +8,9 @@ import ( "os/user" "sync/atomic" - "hakurei.app/container" "hakurei.app/hst" "hakurei.app/internal/app/state" + "hakurei.app/message" "hakurei.app/system" ) @@ -37,7 +37,7 @@ type outcome struct { syscallDispatcher } -func (k *outcome) finalise(ctx context.Context, msg container.Msg, id *state.ID, config *hst.Config) error { +func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *state.ID, config *hst.Config) error { if ctx == nil || id == nil { // unreachable panic("invalid call to finalise") diff --git a/internal/app/hsu.go b/internal/app/hsu.go index 982cb51b..9f0408ac 100644 --- a/internal/app/hsu.go +++ b/internal/app/hsu.go @@ -9,9 +9,9 @@ import ( "strconv" "sync" - "hakurei.app/container" "hakurei.app/container/fhs" "hakurei.app/hst" + "hakurei.app/message" ) // Hsu caches responses from cmd/hsu. @@ -74,7 +74,7 @@ func (h *Hsu) ID() (int, error) { func (h *Hsu) MustID() int { return h.MustIDMsg(nil) } // MustIDMsg implements MustID with a custom [container.Msg]. -func (h *Hsu) MustIDMsg(msg container.Msg) int { +func (h *Hsu) MustIDMsg(msg message.Msg) int { id, err := h.ID() if err == nil { return id @@ -87,7 +87,7 @@ func (h *Hsu) MustIDMsg(msg container.Msg) int { } os.Exit(1) return -0xdeadbeef - } else if m, ok := container.GetErrorMessage(err); ok { + } else if m, ok := message.GetMessage(err); ok { log.Fatal(m) return -0xdeadbeef } else { diff --git a/internal/app/outcome.go b/internal/app/outcome.go index f029a4b9..f4589938 100644 --- a/internal/app/outcome.go +++ b/internal/app/outcome.go @@ -8,6 +8,7 @@ import ( "hakurei.app/container/check" "hakurei.app/hst" "hakurei.app/internal/app/state" + "hakurei.app/message" "hakurei.app/system" "hakurei.app/system/acl" ) @@ -61,7 +62,7 @@ type outcomeState struct { // Copied via populateLocal. k syscallDispatcher // Copied via populateLocal. - msg container.Msg + msg message.Msg } // valid checks outcomeState to be safe for use with outcomeOp. @@ -75,7 +76,7 @@ func (s *outcomeState) valid() bool { // populateEarly populates exported fields via syscallDispatcher. // This must only be called from the priv side. -func (s *outcomeState) populateEarly(k syscallDispatcher, msg container.Msg, config *hst.Config) { +func (s *outcomeState) populateEarly(k syscallDispatcher, msg message.Msg, config *hst.Config) { s.Shim = &shimParams{PrivPID: os.Getpid(), Verbose: msg.IsVerbose(), Ops: fromConfig(config)} // enforce bounds and default early @@ -98,7 +99,7 @@ func (s *outcomeState) populateEarly(k syscallDispatcher, msg container.Msg, con // populateLocal populates unexported fields from transmitted exported fields. // These fields are cheaper to recompute per-process. -func (s *outcomeState) populateLocal(k syscallDispatcher, msg container.Msg) error { +func (s *outcomeState) populateLocal(k syscallDispatcher, msg message.Msg) error { if !s.valid() || k == nil || msg == nil { return newWithMessage("impossible outcome state reached") } diff --git a/internal/app/process.go b/internal/app/process.go index 32254969..0962e212 100644 --- a/internal/app/process.go +++ b/internal/app/process.go @@ -17,6 +17,7 @@ import ( "hakurei.app/hst" "hakurei.app/internal" "hakurei.app/internal/app/state" + "hakurei.app/message" "hakurei.app/system" ) @@ -40,7 +41,7 @@ type mainState struct { cmdWait chan error k *outcome - container.Msg + message.Msg uintptr } @@ -207,7 +208,7 @@ func (ms mainState) fatal(fallback string, ferr error) { } // main carries out outcome and terminates. main does not return. -func (k *outcome) main(msg container.Msg) { +func (k *outcome) main(msg message.Msg) { if !k.active.CompareAndSwap(false, true) { panic("outcome: attempted to run twice") } @@ -312,10 +313,10 @@ func (k *outcome) main(msg container.Msg) { os.Exit(0) } -// printMessageError prints the error message according to [container.GetErrorMessage], +// printMessageError prints the error message according to [message.GetMessage], // or fallback prepended to err if an error message is not available. func printMessageError(fallback string, err error) { - m, ok := container.GetErrorMessage(err) + m, ok := message.GetMessage(err) if !ok { log.Println(fallback, err) return diff --git a/internal/app/shim.go b/internal/app/shim.go index 52aa5308..27305e66 100644 --- a/internal/app/shim.go +++ b/internal/app/shim.go @@ -18,6 +18,7 @@ import ( "hakurei.app/container/bits" "hakurei.app/container/seccomp" "hakurei.app/hst" + "hakurei.app/message" ) //#include "shim-signal.h" @@ -57,7 +58,7 @@ func (p *shimParams) valid() bool { func ShimMain() { log.SetPrefix("shim: ") log.SetFlags(0) - msg := container.NewMsg(log.Default()) + msg := message.NewMsg(log.Default()) if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil { log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err) @@ -81,7 +82,7 @@ func ShimMain() { closeSetup = f if err = state.populateLocal(direct{}, msg); err != nil { - if m, ok := container.GetErrorMessage(err); ok { + if m, ok := message.GetMessage(err); ok { log.Fatal(m) } else { log.Fatalf("cannot populate local state: %v", err) @@ -114,7 +115,7 @@ func ShimMain() { } for _, op := range state.Shim.Ops { if err := op.toContainer(&stateParams); err != nil { - if m, ok := container.GetErrorMessage(err); ok { + if m, ok := message.GetMessage(err); ok { log.Fatal(m) } else { log.Fatalf("cannot create container state: %v", err) diff --git a/internal/app/spcontainer.go b/internal/app/spcontainer.go index 3827fa2e..ee2833bf 100644 --- a/internal/app/spcontainer.go +++ b/internal/app/spcontainer.go @@ -15,6 +15,7 @@ import ( "hakurei.app/container/fhs" "hakurei.app/container/seccomp" "hakurei.app/hst" + "hakurei.app/message" "hakurei.app/system/dbus" ) @@ -299,7 +300,7 @@ func resolveRoot(c *hst.ContainerConfig) (rootfs hst.FilesystemConfig, filesyste } // evalSymlinks calls syscallDispatcher.evalSymlinks but discards errors unwrapping to [fs.ErrNotExist]. -func evalSymlinks(msg container.Msg, k syscallDispatcher, v *string) error { +func evalSymlinks(msg message.Msg, k syscallDispatcher, v *string) error { if p, err := k.evalSymlinks(*v); err != nil { if !errors.Is(err, fs.ErrNotExist) { return err diff --git a/internal/app/state/multi.go b/internal/app/state/multi.go index 244b5c56..ca608f58 100644 --- a/internal/app/state/multi.go +++ b/internal/app/state/multi.go @@ -11,8 +11,8 @@ import ( "sync" "syscall" - "hakurei.app/container" "hakurei.app/hst" + "hakurei.app/message" ) // fine-grained locking and access @@ -22,7 +22,7 @@ type multiStore struct { // initialised backends backends *sync.Map - msg container.Msg + msg message.Msg mu sync.RWMutex } @@ -280,7 +280,7 @@ func (b *multiBackend) close() error { } // NewMulti returns an instance of the multi-file store. -func NewMulti(msg container.Msg, runDir string) Store { +func NewMulti(msg message.Msg, runDir string) Store { return &multiStore{ msg: msg, base: path.Join(runDir, "state"), diff --git a/internal/app/state/multi_test.go b/internal/app/state/multi_test.go index b8e1db9b..fd994438 100644 --- a/internal/app/state/multi_test.go +++ b/internal/app/state/multi_test.go @@ -4,10 +4,10 @@ import ( "log" "testing" - "hakurei.app/container" "hakurei.app/internal/app/state" + "hakurei.app/message" ) func TestMulti(t *testing.T) { - testStore(t, state.NewMulti(container.NewMsg(log.New(log.Writer(), "multi: ", 0)), t.TempDir())) + testStore(t, state.NewMulti(message.NewMsg(log.New(log.Writer(), "multi: ", 0)), t.TempDir())) } diff --git a/ldd/exec.go b/ldd/exec.go index 5554d9b0..ef5bf7b8 100644 --- a/ldd/exec.go +++ b/ldd/exec.go @@ -13,6 +13,7 @@ import ( "hakurei.app/container/check" "hakurei.app/container/fhs" "hakurei.app/container/seccomp" + "hakurei.app/message" ) const ( @@ -25,7 +26,7 @@ var ( msgStaticGlibc = []byte("not a dynamic executable") ) -func Exec(ctx context.Context, msg container.Msg, p string) ([]*Entry, error) { +func Exec(ctx context.Context, msg message.Msg, p string) ([]*Entry, error) { c, cancel := context.WithTimeout(ctx, lddTimeout) defer cancel() diff --git a/message/message.go b/message/message.go new file mode 100644 index 00000000..36f8ae4b --- /dev/null +++ b/message/message.go @@ -0,0 +1,111 @@ +// Package message provides interfaces and a base implementation for extended reporting on top of [log.Logger] +package message + +import ( + "errors" + "log" + "sync/atomic" +) + +// Error is an error with a user-facing message. +type Error interface { + // Message returns a user-facing error message. + Message() string + + error +} + +// GetMessage returns whether an error implements [Error], and the message if it does. +func GetMessage(err error) (string, bool) { + var e Error + if !errors.As(err, &e) || e == nil { + return "", 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/message/message_test.go b/message/message_test.go new file mode 100644 index 00000000..93ceaacb --- /dev/null +++ b/message/message_test.go @@ -0,0 +1,178 @@ +package message_test + +import ( + "bytes" + "errors" + "io" + "log" + "strings" + "syscall" + "testing" + + "hakurei.app/container" + "hakurei.app/container/stub" + "hakurei.app/message" +) + +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 := message.GetMessage(tc.err) + if got != tc.want { + t.Errorf("GetMessage: %q, want %q", got, tc.want) + } + if ok != tc.wantOk { + t.Errorf("GetMessage: 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 := message.NewMsg(nil).GetLogger() + + if out := got.Writer().(*message.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 := message.NewMsg(l) + + if logger := got.GetLogger(); logger != l { + t.Errorf("GetLogger: %#v, want %#v", logger, l) + } + + if ds := l.Writer().(*message.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 message.Msg) + }{ + {"zero verbose", nil, nil, nil, func(t *testing.T, msg message.Msg) { + if msg.IsVerbose() { + t.Error("IsVerbose unexpected true") + } + }}, + + {"swap false", nil, nil, nil, func(t *testing.T, msg message.Msg) { + if msg.SwapVerbose(false) { + t.Error("SwapVerbose unexpected true") + } + }}, + {"write discard", nil, nil, nil, func(_ *testing.T, msg message.Msg) { + msg.Verbose("\x00") + msg.Verbosef("\x00") + }}, + {"verbose false", nil, nil, nil, func(t *testing.T, msg message.Msg) { + if msg.IsVerbose() { + t.Error("IsVerbose unexpected true") + } + }}, + + {"swap true", nil, nil, nil, func(t *testing.T, msg message.Msg) { + if msg.SwapVerbose(true) { + t.Error("SwapVerbose unexpected true") + } + }}, + {"write verbose", []byte("test: \x00\n"), nil, nil, func(_ *testing.T, msg message.Msg) { + msg.Verbose("\x00") + }}, + {"write verbosef", []byte(`test: "\x00"` + "\n"), nil, nil, func(_ *testing.T, msg message.Msg) { + msg.Verbosef("%q", "\x00") + }}, + {"verbose true", nil, nil, nil, func(t *testing.T, msg message.Msg) { + if !msg.IsVerbose() { + t.Error("IsVerbose unexpected false") + } + }}, + + {"resume noop", nil, nil, nil, func(t *testing.T, msg message.Msg) { + if msg.Resume() { + t.Error("Resume unexpected success") + } + }}, + {"beforeExit noop", nil, nil, nil, func(_ *testing.T, msg message.Msg) { + msg.BeforeExit() + }}, + + {"beforeExit suspend", nil, nil, nil, func(_ *testing.T, msg message.Msg) { + msg.Suspend() + }}, + {"beforeExit message", []byte("test: beforeExit reached on suspended output\n"), nil, nil, func(_ *testing.T, msg message.Msg) { + msg.BeforeExit() + }}, + {"post beforeExit resume noop", nil, nil, nil, func(t *testing.T, msg message.Msg) { + if msg.Resume() { + t.Error("Resume unexpected success") + } + }}, + + {"suspend", nil, nil, nil, func(_ *testing.T, msg message.Msg) { + msg.Suspend() + }}, + {"suspend write", nil, nil, nil, func(_ *testing.T, msg message.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 message.Msg) { + if !msg.Resume() { + t.Error("Resume unexpected failure") + } + }}, + + {"suspend drop", nil, nil, nil, func(_ *testing.T, msg message.Msg) { + msg.Suspend() + }}, + {"suspend write fill", nil, nil, nil, func(_ *testing.T, msg message.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 message.Msg) { + if !msg.Resume() { + t.Error("Resume unexpected failure") + } + }}, + } + + msg := message.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/message/output.go b/message/output.go new file mode 100644 index 00000000..cb8df57c --- /dev/null +++ b/message/output.go @@ -0,0 +1,77 @@ +package message + +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/message/output_test.go b/message/output_test.go new file mode 100644 index 00000000..ab69fe52 --- /dev/null +++ b/message/output_test.go @@ -0,0 +1,173 @@ +package message_test + +import ( + "bytes" + "errors" + "reflect" + "strconv" + "syscall" + "testing" + + "hakurei.app/container/stub" + "hakurei.app/message" +) + +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 := message.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/system/dbus/dbus_test.go b/system/dbus/dbus_test.go index 978b55e1..299a4637 100644 --- a/system/dbus/dbus_test.go +++ b/system/dbus/dbus_test.go @@ -11,8 +11,8 @@ import ( "testing" "time" - "hakurei.app/container" "hakurei.app/helper" + "hakurei.app/message" "hakurei.app/system/dbus" ) @@ -93,9 +93,9 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) { t.Run("invalid start", func(t *testing.T) { if !useSandbox { - p = dbus.NewDirect(t.Context(), container.NewMsg(nil), nil, nil) + p = dbus.NewDirect(t.Context(), message.NewMsg(nil), nil, nil) } else { - p = dbus.New(t.Context(), container.NewMsg(nil), nil, nil) + p = dbus.New(t.Context(), message.NewMsg(nil), nil, nil) } if err := p.Start(); !errors.Is(err, syscall.ENOTRECOVERABLE) { @@ -128,9 +128,9 @@ func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) { defer cancel() output := new(strings.Builder) if !useSandbox { - p = dbus.NewDirect(ctx, container.NewMsg(nil), final, output) + p = dbus.NewDirect(ctx, message.NewMsg(nil), final, output) } else { - p = dbus.New(ctx, container.NewMsg(nil), final, output) + p = dbus.New(ctx, message.NewMsg(nil), final, output) } t.Run("invalid wait", func(t *testing.T) { diff --git a/system/dbus/export_test.go b/system/dbus/export_test.go index b1dc365f..bc24f3f3 100644 --- a/system/dbus/export_test.go +++ b/system/dbus/export_test.go @@ -4,11 +4,11 @@ import ( "context" "io" - "hakurei.app/container" + "hakurei.app/message" ) // NewDirect returns a new instance of [Proxy] with its sandbox disabled. -func NewDirect(ctx context.Context, msg container.Msg, final *Final, output io.Writer) *Proxy { +func NewDirect(ctx context.Context, msg message.Msg, final *Final, output io.Writer) *Proxy { p := New(ctx, msg, final, output) p.useSandbox = false return p diff --git a/system/dbus/proxy.go b/system/dbus/proxy.go index 535f233d..385a8c7c 100644 --- a/system/dbus/proxy.go +++ b/system/dbus/proxy.go @@ -6,9 +6,9 @@ import ( "sync" "syscall" - "hakurei.app/container" "hakurei.app/helper" "hakurei.app/hst" + "hakurei.app/message" ) // ProxyName is the file name or path to the proxy program. @@ -19,7 +19,7 @@ var ProxyName = "xdg-dbus-proxy" type Proxy struct { helper helper.Helper ctx context.Context - msg container.Msg + msg message.Msg cancel context.CancelCauseFunc cause func() error @@ -100,6 +100,6 @@ func Finalise(sessionBus, systemBus ProxyPair, session, system *hst.BusConfig) ( } // New returns a new instance of [Proxy]. -func New(ctx context.Context, msg container.Msg, final *Final, output io.Writer) *Proxy { +func New(ctx context.Context, msg message.Msg, final *Final, output io.Writer) *Proxy { return &Proxy{name: ProxyName, ctx: ctx, msg: msg, final: final, output: output, useSandbox: true} } diff --git a/system/output.go b/system/output.go index 022164f0..71acace4 100644 --- a/system/output.go +++ b/system/output.go @@ -6,6 +6,7 @@ import ( "os" "hakurei.app/container" + "hakurei.app/message" ) // OpError is returned by [I.Commit] and [I.Revert]. @@ -70,14 +71,14 @@ func printJoinedError(println func(v ...any), fallback string, err error) { error } if !errors.As(err, &joinErr) { - if m, ok := container.GetErrorMessage(err); ok { + if m, ok := message.GetMessage(err); ok { println(m) } else { println(fallback, err) } } else { for _, err = range joinErr.Unwrap() { - if m, ok := container.GetErrorMessage(err); ok { + if m, ok := message.GetMessage(err); ok { println(m) } else { println(err.Error()) diff --git a/system/output_test.go b/system/output_test.go index b144d79c..d395b13c 100644 --- a/system/output_test.go +++ b/system/output_test.go @@ -9,6 +9,7 @@ import ( "testing" "hakurei.app/container" + "hakurei.app/message" ) func TestOpError(t *testing.T) { @@ -64,13 +65,13 @@ func TestOpError(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) } }) }) diff --git a/system/system.go b/system/system.go index 5599218e..cadaab04 100644 --- a/system/system.go +++ b/system/system.go @@ -6,8 +6,8 @@ import ( "errors" "strings" - "hakurei.app/container" "hakurei.app/hst" + "hakurei.app/message" ) const ( @@ -68,7 +68,7 @@ func TypeString(e hst.Enablement) string { } // New returns the address of a new [I] targeting uid. -func New(ctx context.Context, msg container.Msg, uid int) (sys *I) { +func New(ctx context.Context, msg message.Msg, uid int) (sys *I) { if ctx == nil || msg == nil || uid < 0 { panic("invalid call to New") } @@ -89,7 +89,7 @@ type I struct { // the behaviour of Revert is only defined for up to one call reverted bool - msg container.Msg + msg message.Msg syscallDispatcher } diff --git a/system/system_test.go b/system/system_test.go index 5a361b95..91b68565 100644 --- a/system/system_test.go +++ b/system/system_test.go @@ -8,10 +8,10 @@ import ( "strconv" "testing" - "hakurei.app/container" "hakurei.app/container/check" "hakurei.app/container/stub" "hakurei.app/hst" + "hakurei.app/message" "hakurei.app/system/internal/xcb" ) @@ -74,7 +74,7 @@ func TestNew(t *testing.T) { t.Errorf("recover: %v, want %v", r, want) } }() - New(nil, container.NewMsg(nil), 0) + New(nil, message.NewMsg(nil), 0) }) t.Run("msg", func(t *testing.T) { @@ -94,11 +94,11 @@ func TestNew(t *testing.T) { t.Errorf("recover: %v, want %v", r, want) } }() - New(t.Context(), container.NewMsg(nil), -1) + New(t.Context(), message.NewMsg(nil), -1) }) }) - sys := New(t.Context(), container.NewMsg(nil), 0xdeadbeef) + sys := New(t.Context(), message.NewMsg(nil), 0xdeadbeef) if sys.ctx == nil { t.Error("New: ctx = nil") } @@ -115,51 +115,51 @@ func TestEqual(t *testing.T) { want bool }{ {"simple UID", - New(t.Context(), container.NewMsg(nil), 150), - New(t.Context(), container.NewMsg(nil), 150), + New(t.Context(), message.NewMsg(nil), 150), + New(t.Context(), message.NewMsg(nil), 150), true}, {"simple UID differ", - New(t.Context(), container.NewMsg(nil), 150), - New(t.Context(), container.NewMsg(nil), 151), + New(t.Context(), message.NewMsg(nil), 150), + New(t.Context(), message.NewMsg(nil), 151), false}, {"simple UID nil", - New(t.Context(), container.NewMsg(nil), 150), + New(t.Context(), message.NewMsg(nil), 150), nil, false}, {"op length mismatch", - New(t.Context(), container.NewMsg(nil), 150). + New(t.Context(), message.NewMsg(nil), 150). ChangeHosts("chronos"), - New(t.Context(), container.NewMsg(nil), 150). + New(t.Context(), message.NewMsg(nil), 150). ChangeHosts("chronos"). Ensure(m("/run"), 0755), false}, {"op value mismatch", - New(t.Context(), container.NewMsg(nil), 150). + New(t.Context(), message.NewMsg(nil), 150). ChangeHosts("chronos"). Ensure(m("/run"), 0644), - New(t.Context(), container.NewMsg(nil), 150). + New(t.Context(), message.NewMsg(nil), 150). ChangeHosts("chronos"). Ensure(m("/run"), 0755), false}, {"op type mismatch", - New(t.Context(), container.NewMsg(nil), 150). + New(t.Context(), message.NewMsg(nil), 150). ChangeHosts("chronos"). CopyFile(new([]byte), m("/home/ophestra/xdg/config/pulse/cookie"), 0, 256), - New(t.Context(), container.NewMsg(nil), 150). + New(t.Context(), message.NewMsg(nil), 150). ChangeHosts("chronos"). Ensure(m("/run"), 0755), false}, {"op equals", - New(t.Context(), container.NewMsg(nil), 150). + New(t.Context(), message.NewMsg(nil), 150). ChangeHosts("chronos"). Ensure(m("/run"), 0755), - New(t.Context(), container.NewMsg(nil), 150). + New(t.Context(), message.NewMsg(nil), 150). ChangeHosts("chronos"). Ensure(m("/run"), 0755), true}, -- cgit v1.3.1