diff --git a/hst/hst.go b/hst/hst.go index d9c1168..c9a23d7 100644 --- a/hst/hst.go +++ b/hst/hst.go @@ -2,12 +2,42 @@ package hst import ( + "errors" + "net" + "os" + "hakurei.app/container" "hakurei.app/container/seccomp" "hakurei.app/system" "hakurei.app/system/dbus" ) +// An AppError is returned while starting an app according to [hst.Config]. +type AppError struct { + Step string + Err error + Msg string +} + +func (e *AppError) Error() string { return e.Err.Error() } +func (e *AppError) Unwrap() error { return e.Err } +func (e *AppError) Message() string { + if e.Msg != "" { + return e.Msg + } + + switch { + case errors.As(e.Err, new(*os.PathError)), + errors.As(e.Err, new(*os.LinkError)), + errors.As(e.Err, new(*os.SyscallError)), + errors.As(e.Err, new(*net.OpError)): + return "cannot " + e.Error() + + default: + return "cannot " + e.Step + ": " + e.Error() + } +} + // Paths contains environment-dependent paths used by hakurei. type Paths struct { // temporary directory returned by [os.TempDir] (usually `/tmp`) diff --git a/hst/hst_test.go b/hst/hst_test.go index 0405721..70f7e53 100644 --- a/hst/hst_test.go +++ b/hst/hst_test.go @@ -2,11 +2,93 @@ package hst_test import ( "encoding/json" + "errors" + "net" + "os" + "syscall" "testing" + "hakurei.app/container" + "hakurei.app/container/stub" "hakurei.app/hst" ) +func TestAppError(t *testing.T) { + testCases := []struct { + name string + err error + s string + message string + is, isF error + }{ + {"message", &hst.AppError{Step: "obtain uid from hsu", Err: stub.UniqueError(0), + Msg: "the setuid helper is missing: /run/wrappers/bin/hsu"}, + "unique error 0 injected by the test suite", + "the setuid helper is missing: /run/wrappers/bin/hsu", + stub.UniqueError(0), os.ErrNotExist}, + + {"os.PathError", &hst.AppError{Step: "passthrough os.PathError", + Err: &os.PathError{Op: "stat", Path: "/proc/nonexistent", Err: os.ErrNotExist}}, + "stat /proc/nonexistent: file does not exist", + "cannot stat /proc/nonexistent: file does not exist", + os.ErrNotExist, stub.UniqueError(0xdeadbeef)}, + + {"os.LinkError", &hst.AppError{Step: "passthrough os.LinkError", + Err: &os.LinkError{Op: "link", Old: "/proc/self", New: "/proc/nonexistent", Err: os.ErrNotExist}}, + "link /proc/self /proc/nonexistent: file does not exist", + "cannot link /proc/self /proc/nonexistent: file does not exist", + os.ErrNotExist, stub.UniqueError(0xdeadbeef)}, + + {"os.SyscallError", &hst.AppError{Step: "passthrough os.SyscallError", + Err: &os.SyscallError{Syscall: "meow", Err: syscall.ENOSYS}}, + "meow: function not implemented", + "cannot meow: function not implemented", + syscall.ENOSYS, syscall.ENOTRECOVERABLE}, + + {"net.OpError", &hst.AppError{Step: "passthrough net.OpError", + Err: &net.OpError{Op: "dial", Net: "cat", Err: net.UnknownNetworkError("cat")}}, + "dial cat: unknown network cat", + "cannot dial cat: unknown network cat", + net.UnknownNetworkError("cat"), syscall.ENOTRECOVERABLE}, + + {"default", &hst.AppError{Step: "initialise container configuration", Err: stub.UniqueError(1)}, + "unique error 1 injected by the test suite", + "cannot initialise container configuration: unique error 1 injected by the test suite", + stub.UniqueError(1), os.ErrInvalid}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Run("error", func(t *testing.T) { + if got := tc.err.Error(); got != tc.s { + t.Errorf("Error: %s, want %s", got, tc.s) + } + }) + + t.Run("message", func(t *testing.T) { + gotMessage, gotMessageOk := container.GetErrorMessage(tc.err) + if want := tc.message != "\x00"; gotMessageOk != want { + t.Errorf("GetErrorMessage: ok = %v, want %v", gotMessage, want) + } + + if gotMessageOk { + if gotMessage != tc.message { + t.Errorf("GetErrorMessage: %s, want %s", gotMessage, tc.message) + } + } + }) + + t.Run("is", func(t *testing.T) { + if !errors.Is(tc.err, tc.is) { + t.Errorf("Is: unexpected false for %v", tc.is) + } + if errors.Is(tc.err, tc.isF) { + t.Errorf("Is: unexpected true for %v", tc.isF) + } + }) + }) + } +} + func TestTemplate(t *testing.T) { const want = `{ "id": "org.chromium.Chromium", diff --git a/internal/app/errors.go b/internal/app/errors.go index 7a95508..566103d 100644 --- a/internal/app/errors.go +++ b/internal/app/errors.go @@ -5,6 +5,7 @@ import ( "log" "hakurei.app/container" + "hakurei.app/hst" "hakurei.app/internal/hlog" ) @@ -30,7 +31,7 @@ func PrintRunStateErr(rs *RunState, runErr error) (code int) { } else { // InnerErr is returned by c.Save(&sd, seal.ct), and are always unwrapped printMessageError("error returned during revert:", - &FinaliseError{Step: "save process state", Err: se.InnerErr}) + &hst.AppError{Step: "save process state", Err: se.InnerErr}) } } } @@ -133,7 +134,7 @@ func (e *StateStoreError) equiv(step string) error { if e.Inner && e.InnerErr == nil && e.DoErr == nil && e.OpErr == nil && errors.Join(e.Errs...) == nil { return nil } else { - return &FinaliseError{Step: step, Err: e} + return &hst.AppError{Step: step, Err: e} } } diff --git a/internal/app/process.go b/internal/app/process.go index f67327f..50d5dc8 100644 --- a/internal/app/process.go +++ b/internal/app/process.go @@ -13,6 +13,7 @@ import ( "time" "hakurei.app/container" + "hakurei.app/hst" "hakurei.app/internal" "hakurei.app/internal/app/state" "hakurei.app/internal/hlog" @@ -119,7 +120,7 @@ func (seal *Outcome) Run(rs *RunState) error { var e *gob.Encoder if fd, encoder, err := container.Setup(&cmd.ExtraFiles); err != nil { - return &FinaliseError{Step: "create shim setup pipe", Err: err} + return &hst.AppError{Step: "create shim setup pipe", Err: err} } else { e = encoder cmd.Env = []string{ @@ -139,7 +140,7 @@ func (seal *Outcome) Run(rs *RunState) error { hlog.Verbosef("setuid helper at %s", hsuPath) hlog.Suspend() if err := cmd.Start(); err != nil { - return &FinaliseError{Step: "start setuid wrapper", Err: err} + return &hst.AppError{Step: "start setuid wrapper", Err: err} } rs.setStart() @@ -159,7 +160,7 @@ func (seal *Outcome) Run(rs *RunState) error { case err := <-setupErr: if err != nil { hlog.Resume() - return &FinaliseError{Step: "transmit shim config", Err: err} + return &hst.AppError{Step: "transmit shim config", Err: err} } case <-ctx.Done(): diff --git a/internal/app/seal.go b/internal/app/seal.go index 0552a54..e6cccec 100644 --- a/internal/app/seal.go +++ b/internal/app/seal.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "io/fs" - "net" "os" "slices" "strconv" @@ -28,35 +27,9 @@ import ( "hakurei.app/system/wayland" ) -// A FinaliseError is returned while finalising a [hst.Config] outcome. -type FinaliseError struct { - Step string - Err error - Msg string -} - -func (e *FinaliseError) Error() string { return e.Err.Error() } -func (e *FinaliseError) Unwrap() error { return e.Err } -func (e *FinaliseError) Message() string { - if e.Msg != "" { - return e.Msg - } - - switch { - case errors.As(e.Err, new(*os.PathError)), - errors.As(e.Err, new(*os.LinkError)), - errors.As(e.Err, new(*os.SyscallError)), - errors.As(e.Err, new(*net.OpError)): - return "cannot " + e.Error() - - default: - return "cannot " + e.Step + ": " + e.Error() - } -} - func newWithMessage(msg string) error { return newWithMessageError(msg, os.ErrInvalid) } func newWithMessageError(msg string, err error) error { - return &FinaliseError{Step: "finalise", Err: err, Msg: msg} + return &hst.AppError{Step: "finalise", Err: err, Msg: msg} } // An Outcome is the runnable state of a hakurei container via [hst.Config]. @@ -188,7 +161,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co // encode initial configuration for state tracking ct := new(bytes.Buffer) if err := gob.NewEncoder(ct).Encode(config); err != nil { - return &FinaliseError{Step: "encode initial config", Err: err} + return &hst.AppError{Step: "encode initial config", Err: err} } seal.ct = ct } @@ -238,7 +211,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co if config.Path == nil { if len(config.Args) > 0 { if p, err := sys.LookPath(config.Args[0]); err != nil { - return &FinaliseError{Step: "look up executable file", Err: err} + return &hst.AppError{Step: "look up executable file", Err: err} } else if config.Path, err = container.NewAbs(p); err != nil { return newWithMessageError(err.Error(), err) } @@ -304,7 +277,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co seal.container, seal.env, err = newContainer(config.Container, sys, seal.id.String(), &uid, &gid) seal.waitDelay = config.Container.WaitDelay if err != nil { - return &FinaliseError{Step: "initialise container configuration", Err: err} + return &hst.AppError{Step: "initialise container configuration", Err: err} } if len(config.Args) == 0 { config.Args = []string{config.Path.String()} @@ -427,7 +400,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co if socketPath != nil { if _, err := sys.Stat(socketPath.String()); err != nil { if !errors.Is(err, fs.ErrNotExist) { - return &FinaliseError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err} + return &hst.AppError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err} } } else { seal.sys.UpdatePermType(system.EX11, socketPath.String(), acl.Read, acl.Write, acl.Execute) @@ -451,14 +424,14 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co if _, err := sys.Stat(pulseRuntimeDir.String()); err != nil { if !errors.Is(err, fs.ErrNotExist) { - return &FinaliseError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err} + return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err} } return newWithMessage(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir)) } if s, err := sys.Stat(pulseSocket.String()); err != nil { if !errors.Is(err, fs.ErrNotExist) { - return &FinaliseError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err} + return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err} } return newWithMessage(fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir)) } else { @@ -482,7 +455,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co // from environment if p, ok := sys.LookupEnv(pulseCookie); ok { if a, err := container.NewAbs(p); err != nil { - return &FinaliseError{Step: paLocateStep, Err: err} + return &hst.AppError{Step: paLocateStep, Err: err} } else { // this takes precedence, do not verify whether the file is accessible paCookiePath = a @@ -493,7 +466,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co // $HOME/.pulse-cookie if p, ok := sys.LookupEnv(home); ok { if a, err := container.NewAbs(p); err != nil { - return &FinaliseError{Step: paLocateStep, Err: err} + return &hst.AppError{Step: paLocateStep, Err: err} } else { paCookiePath = a.Append(".pulse-cookie") } @@ -501,7 +474,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co if s, err := sys.Stat(paCookiePath.String()); err != nil { paCookiePath = nil if !errors.Is(err, fs.ErrNotExist) { - return &FinaliseError{Step: "access PulseAudio cookie", Err: err} + return &hst.AppError{Step: "access PulseAudio cookie", Err: err} } // fallthrough } else if s.IsDir() { @@ -514,14 +487,14 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co // $XDG_CONFIG_HOME/pulse/cookie if p, ok := sys.LookupEnv(xdgConfigHome); ok { if a, err := container.NewAbs(p); err != nil { - return &FinaliseError{Step: paLocateStep, Err: err} + return &hst.AppError{Step: paLocateStep, Err: err} } else { paCookiePath = a.Append("pulse", "cookie") } if s, err := sys.Stat(paCookiePath.String()); err != nil { paCookiePath = nil if !errors.Is(err, fs.ErrNotExist) { - return &FinaliseError{Step: "access PulseAudio cookie", Err: err} + return &hst.AppError{Step: "access PulseAudio cookie", Err: err} } // fallthrough } else if s.IsDir() { @@ -609,7 +582,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co seal.container.Env = make([]string, 0, len(seal.env)) for k, v := range seal.env { if strings.IndexByte(k, '=') != -1 { - return &FinaliseError{Step: "flatten environment", Err: syscall.EINVAL, + return &hst.AppError{Step: "flatten environment", Err: syscall.EINVAL, Msg: fmt.Sprintf("invalid environment variable %s", k)} } seal.container.Env = append(seal.container.Env, k+"="+v)