container/msg: optionally provide error messages
All checks were successful
Test / Create distribution (push) Successful in 35s
Test / Sandbox (push) Successful in 2m18s
Test / Hakurei (push) Successful in 3m22s
Test / Hpkg (push) Successful in 3m43s
Test / Sandbox (race detector) (push) Successful in 4m20s
Test / Hakurei (race detector) (push) Successful in 5m21s
Test / Flake checks (push) Successful in 1m38s

This makes handling of fatal errors a lot less squirmy.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-08-31 11:57:59 +09:00
parent 712cfc06d7
commit 780e3e5465
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
4 changed files with 117 additions and 8 deletions

View File

@ -132,6 +132,24 @@ func (e *StartError) Error() string {
return e.Step + ": " + e.Err.Error()
}
// Message returns a user-facing error message.
func (e *StartError) Message() string {
if e.Passthrough {
switch {
case errors.As(e.Err, new(*os.PathError)),
errors.As(e.Err, new(*os.SyscallError)):
return "cannot " + e.Err.Error()
default:
return e.Err.Error()
}
}
if e.Origin {
return e.Step
}
return "cannot " + e.Error()
}
// Start starts the container init. The init process blocks until Serve is called.
func (p *Container) Start() error {
if p.cmd != nil {

View File

@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"log"
"net"
"os"
"os/exec"
"os/signal"
@ -34,6 +35,7 @@ func TestStartError(t *testing.T) {
s string
is error
isF error
msg string
}{
{"params env", &container.StartError{
Fatal: true,
@ -41,7 +43,8 @@ func TestStartError(t *testing.T) {
Err: container.ErrReceiveEnv,
},
"set up params stream: environment variable not set",
container.ErrReceiveEnv, syscall.EBADF},
container.ErrReceiveEnv, syscall.EBADF,
"cannot set up params stream: environment variable not set"},
{"params", &container.StartError{
Fatal: true,
@ -49,7 +52,8 @@ func TestStartError(t *testing.T) {
Err: &os.SyscallError{Syscall: "pipe2", Err: syscall.EBADF},
},
"set up params stream pipe2: bad file descriptor",
syscall.EBADF, os.ErrInvalid},
syscall.EBADF, os.ErrInvalid,
"cannot set up params stream pipe2: bad file descriptor"},
{"PR_SET_NO_NEW_PRIVS", &container.StartError{
Fatal: true,
@ -57,14 +61,16 @@ func TestStartError(t *testing.T) {
Err: syscall.EPERM,
},
"prctl(PR_SET_NO_NEW_PRIVS): operation not permitted",
syscall.EPERM, syscall.EACCES},
syscall.EPERM, syscall.EACCES,
"cannot prctl(PR_SET_NO_NEW_PRIVS): operation not permitted"},
{"landlock abi", &container.StartError{
Step: "get landlock ABI",
Err: syscall.ENOSYS,
},
"get landlock ABI: function not implemented",
syscall.ENOSYS, syscall.ENOEXEC},
syscall.ENOSYS, syscall.ENOEXEC,
"cannot get landlock ABI: function not implemented"},
{"landlock old", &container.StartError{
Step: "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
@ -72,7 +78,8 @@ func TestStartError(t *testing.T) {
Origin: true,
},
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
syscall.ENOSYS, syscall.ENOSPC},
syscall.ENOSYS, syscall.ENOSPC,
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET"},
{"landlock create", &container.StartError{
Fatal: true,
@ -80,7 +87,8 @@ func TestStartError(t *testing.T) {
Err: syscall.EBADFD,
},
"create landlock ruleset: file descriptor in bad state",
syscall.EBADFD, syscall.EBADF},
syscall.EBADFD, syscall.EBADF,
"cannot create landlock ruleset: file descriptor in bad state"},
{"landlock enforce", &container.StartError{
Fatal: true,
@ -88,7 +96,8 @@ func TestStartError(t *testing.T) {
Err: syscall.ENOTRECOVERABLE,
},
"enforce landlock ruleset: state not recoverable",
syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT},
syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT,
"cannot enforce landlock ruleset: state not recoverable"},
{"start", &container.StartError{
Step: "start container init",
@ -99,7 +108,31 @@ func TestStartError(t *testing.T) {
}, Passthrough: true,
},
"fork/exec /proc/nonexistent: no such file or directory",
syscall.ENOENT, syscall.ENOSYS},
syscall.ENOENT, syscall.ENOSYS,
"cannot fork/exec /proc/nonexistent: no such file or directory"},
{"start syscall", &container.StartError{
Step: "start container init",
Err: &os.SyscallError{
Syscall: "open",
Err: syscall.ENOSYS,
}, Passthrough: true,
},
"open: function not implemented",
syscall.ENOSYS, syscall.ENOENT,
"cannot open: function not implemented"},
{"start other", &container.StartError{
Step: "start container init",
Err: &net.OpError{
Op: "dial",
Net: "unix",
Err: syscall.ECONNREFUSED,
}, Passthrough: true,
},
"dial unix: connection refused",
syscall.ECONNREFUSED, syscall.ECONNABORTED,
"dial unix: connection refused"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
@ -117,6 +150,17 @@ func TestStartError(t *testing.T) {
t.Errorf("Is: unexpected true")
}
})
t.Run("msg", func(t *testing.T) {
if got, ok := container.GetErrorMessage(tc.err); !ok {
if tc.msg != "" {
t.Errorf("GetErrorMessage: err does not implement MessageError")
}
return
} else if got != tc.msg {
t.Errorf("GetErrorMessage: %q, want %q", got, tc.msg)
}
})
})
}
}

View File

@ -1,10 +1,28 @@
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
}
type Msg interface {
IsVerbose() bool
Verbose(v ...any)

View File

@ -1,14 +1,43 @@
package container_test
import (
"errors"
"log"
"strings"
"sync/atomic"
"syscall"
"testing"
"hakurei.app/container"
)
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) {
{
w := log.Writer()