internal/wayland: improve error handling
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m21s
Test / Hakurei (push) Successful in 3m20s
Test / Hpkg (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 4m24s
Test / Hakurei (race detector) (push) Successful in 5m16s
Test / Flake checks (push) Successful in 1m32s

Note: wl_registry_add_listener is undocumented everywhere. Its implementation calls wl_proxy_add_listener which returns 0 on success or -1 on failure.
Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-11-15 21:26:31 +09:00
parent 41b49137a8
commit 12751932d1
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
5 changed files with 228 additions and 35 deletions

View File

@ -40,6 +40,10 @@ func (e *OpError) Error() string {
} }
func (e *OpError) Message() string { func (e *OpError) Message() string {
if m, ok := message.GetMessage(e.Err); ok {
return m
}
switch { switch {
case e.Msg != "": case e.Msg != "":
return e.Error() return e.Error()

View File

@ -31,67 +31,84 @@ static const struct wl_registry_listener registry_listener = {
.global_remove = registry_handle_global_remove, .global_remove = registry_handle_global_remove,
}; };
int32_t hakurei_bind_wayland_fd( hakurei_wayland_res hakurei_bind_wayland_fd(
char *socket_path, char *socket_path,
int fd, int fd,
const char *app_id, const char *app_id,
const char *instance_id, const char *instance_id,
int sync_fd) { int sync_fd) {
int32_t res = 0; /* refer to resErr for corresponding Go error */ hakurei_wayland_res res = HAKUREI_WAYLAND_SUCCESS; /* see wayland.go for handling */
struct wl_display *display = NULL;
struct wl_registry *registry;
struct wp_security_context_manager_v1 *security_context_manager = NULL;
int event_cnt;
int listen_fd = -1;
struct sockaddr_un sockaddr = {0};
struct wp_security_context_v1 *security_context;
struct wl_display *display;
display = wl_display_connect_to_fd(fd); display = wl_display_connect_to_fd(fd);
if (!display) { if (display == NULL) {
res = 1; res = HAKUREI_WAYLAND_CONNECT;
goto out; goto out;
}; };
struct wl_registry *registry;
registry = wl_display_get_registry(display); registry = wl_display_get_registry(display);
if (wl_registry_add_listener(registry, &registry_listener, &security_context_manager) < 0) {
struct wp_security_context_manager_v1 *security_context_manager = NULL; res = HAKUREI_WAYLAND_LISTENER;
wl_registry_add_listener(registry, &registry_listener, &security_context_manager);
int ret;
ret = wl_display_roundtrip(display);
wl_registry_destroy(registry);
if (ret < 0)
goto out; goto out;
}
if (!security_context_manager) { event_cnt = wl_display_roundtrip(display);
res = 2; wl_registry_destroy(registry);
if (event_cnt < 0) {
res = HAKUREI_WAYLAND_ROUNDTRIP;
goto out; goto out;
} }
int listen_fd = -1; if (security_context_manager == NULL) {
listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); res = HAKUREI_WAYLAND_NOT_AVAIL;
if (listen_fd < 0)
goto out; goto out;
}
listen_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (listen_fd < 0) {
res = HAKUREI_WAYLAND_SOCKET;
goto out;
}
struct sockaddr_un sockaddr = {0};
sockaddr.sun_family = AF_UNIX; sockaddr.sun_family = AF_UNIX;
snprintf(sockaddr.sun_path, sizeof(sockaddr.sun_path), "%s", socket_path); snprintf(sockaddr.sun_path, sizeof(sockaddr.sun_path), "%s", socket_path);
if (bind(listen_fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != 0) if (bind(listen_fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != 0) {
res = HAKUREI_WAYLAND_BIND;
goto out; goto out;
}
if (listen(listen_fd, 0) != 0) if (listen(listen_fd, 0) != 0) {
res = HAKUREI_WAYLAND_LISTEN;
goto out; goto out;
}
struct wp_security_context_v1 *security_context;
security_context = wp_security_context_manager_v1_create_listener(security_context_manager, listen_fd, sync_fd); security_context = wp_security_context_manager_v1_create_listener(security_context_manager, listen_fd, sync_fd);
if (security_context == NULL) { /* not reached */
res = HAKUREI_WAYLAND_NOT_AVAIL;
goto out;
}
wp_security_context_v1_set_sandbox_engine(security_context, "app.hakurei"); wp_security_context_v1_set_sandbox_engine(security_context, "app.hakurei");
wp_security_context_v1_set_app_id(security_context, app_id); wp_security_context_v1_set_app_id(security_context, app_id);
wp_security_context_v1_set_instance_id(security_context, instance_id); wp_security_context_v1_set_instance_id(security_context, instance_id);
wp_security_context_v1_commit(security_context); wp_security_context_v1_commit(security_context);
wp_security_context_v1_destroy(security_context); wp_security_context_v1_destroy(security_context);
if (wl_display_roundtrip(display) < 0) if (wl_display_roundtrip(display) < 0) {
res = HAKUREI_WAYLAND_ROUNDTRIP;
goto out; goto out;
}
out: out:
if (listen_fd >= 0) if (listen_fd >= 0)
close(listen_fd); close(listen_fd);
if (security_context_manager) if (security_context_manager != NULL)
wp_security_context_manager_v1_destroy(security_context_manager); wp_security_context_manager_v1_destroy(security_context_manager);
if (display) if (display != NULL)
wl_display_disconnect(display); wl_display_disconnect(display);
free((void *)socket_path); free((void *)socket_path);

View File

@ -1,6 +1,22 @@
#include <stdint.h> typedef enum {
HAKUREI_WAYLAND_SUCCESS,
/* wl_display_connect_to_fd failed, errno */
HAKUREI_WAYLAND_CONNECT,
/* wl_registry_add_listener failed, errno */
HAKUREI_WAYLAND_LISTENER,
/* wl_display_roundtrip failed, errno */
HAKUREI_WAYLAND_ROUNDTRIP,
/* compositor does not implement wp_security_context_v1 */
HAKUREI_WAYLAND_NOT_AVAIL,
/* socket failed, errno */
HAKUREI_WAYLAND_SOCKET,
/* bind failed, errno */
HAKUREI_WAYLAND_BIND,
/* listen failed, errno */
HAKUREI_WAYLAND_LISTEN,
} hakurei_wayland_res;
int32_t hakurei_bind_wayland_fd( hakurei_wayland_res hakurei_bind_wayland_fd(
char *socket_path, char *socket_path,
int fd, int fd,
const char *app_id, const char *app_id,

View File

@ -12,7 +12,6 @@ package wayland
*/ */
import "C" import "C"
import ( import (
"errors"
"strings" "strings"
"syscall" "syscall"
) )
@ -31,18 +30,96 @@ const (
FallbackName = "wayland-0" FallbackName = "wayland-0"
) )
var resErr = [...]error{ type (
0: nil, // Res is the outcome of a call to hakurei_bind_wayland_fd.
1: errors.New("wl_display_connect_to_fd() failed"), Res = C.hakurei_wayland_res
2: errors.New("wp_security_context_v1 not available"),
// An Error represents a failure during hakurei_bind_wayland_fd.
Error struct {
// Where the failure occurred.
Cause Res
// Global errno value set during the fault.
Errno error
}
)
// withPrefix returns prefix suffixed with errno description if available.
func (e *Error) withPrefix(prefix string) string {
if e.Errno == nil {
return prefix
}
return prefix + ": " + e.Errno.Error()
} }
const (
// RSuccess is returned on a successful call.
RSuccess Res = C.HAKUREI_WAYLAND_SUCCESS
// RConnect is returned if wl_display_connect_to_fd failed. The global errno is set.
RConnect Res = C.HAKUREI_WAYLAND_CONNECT
// RListener is returned if wl_registry_add_listener failed. The global errno is set.
RListener Res = C.HAKUREI_WAYLAND_LISTENER
// RRoundtrip is returned if wl_display_roundtrip failed. The global errno is set.
RRoundtrip Res = C.HAKUREI_WAYLAND_ROUNDTRIP
// RNotAvail is returned if compositor does not implement wp_security_context_v1.
RNotAvail Res = C.HAKUREI_WAYLAND_NOT_AVAIL
// RSocket is returned if socket failed. The global errno is set.
RSocket Res = C.HAKUREI_WAYLAND_SOCKET
// RBind is returned if bind failed. The global errno is set.
RBind Res = C.HAKUREI_WAYLAND_BIND
// RListen is returned if listen failed. The global errno is set.
RListen Res = C.HAKUREI_WAYLAND_LISTEN
)
func (e *Error) Unwrap() error { return e.Errno }
func (e *Error) Message() string { return e.Error() }
func (e *Error) Error() string {
switch e.Cause {
case RSuccess:
if e.Errno == nil {
return "success"
}
return e.Errno.Error()
case RConnect:
return e.withPrefix("wl_display_connect_to_fd failed")
case RListener:
return e.withPrefix("wl_registry_add_listener failed")
case RRoundtrip:
return e.withPrefix("wl_display_roundtrip failed")
case RNotAvail:
return "compositor does not implement security_context_v1"
case RSocket, RBind, RListen:
if e.Errno == nil {
return "socket operation failed"
}
return e.Errno.Error()
default:
return e.withPrefix("impossible outcome") /* not reached */
}
}
// bindWaylandFd calls hakurei_bind_wayland_fd. A non-nil error has concrete type [Error].
func bindWaylandFd(socketPath string, fd uintptr, appID, instanceID string, syncFd uintptr) error { func bindWaylandFd(socketPath string, fd uintptr, appID, instanceID string, syncFd uintptr) error {
if hasNull(appID) || hasNull(instanceID) { if hasNull(appID) || hasNull(instanceID) {
return syscall.EINVAL return syscall.EINVAL
} }
res := C.hakurei_bind_wayland_fd(C.CString(socketPath), C.int(fd), C.CString(appID), C.CString(instanceID), C.int(syncFd))
return resErr[int32(res)] var e Error
e.Cause, e.Errno = C.hakurei_bind_wayland_fd(
C.CString(socketPath),
C.int(fd),
C.CString(appID),
C.CString(instanceID),
C.int(syncFd),
)
if e.Cause == RSuccess {
return nil
}
return &e
} }
// hasNull returns whether s contains the NUL character.
func hasNull(s string) bool { return strings.IndexByte(s, 0) > -1 } func hasNull(s string) bool { return strings.IndexByte(s, 0) > -1 }

View File

@ -0,0 +1,79 @@
package wayland_test
import (
"syscall"
"testing"
"hakurei.app/container/stub"
"hakurei.app/internal/wayland"
)
func TestError(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err wayland.Error
want string
}{
{"success", wayland.Error{
Cause: wayland.RSuccess,
}, "success"},
{"success errno", wayland.Error{
Cause: wayland.RSuccess,
Errno: stub.UniqueError(0),
}, "unique error 0 injected by the test suite"},
{"wl_display_connect_to_fd", wayland.Error{
Cause: wayland.RConnect,
Errno: stub.UniqueError(1),
}, "wl_display_connect_to_fd failed: unique error 1 injected by the test suite"},
{"wl_registry_add_listener", wayland.Error{
Cause: wayland.RListener,
Errno: stub.UniqueError(2),
}, "wl_registry_add_listener failed: unique error 2 injected by the test suite"},
{"wl_display_roundtrip", wayland.Error{
Cause: wayland.RRoundtrip,
Errno: stub.UniqueError(3),
}, "wl_display_roundtrip failed: unique error 3 injected by the test suite"},
{"not available", wayland.Error{
Cause: wayland.RNotAvail,
}, "compositor does not implement security_context_v1"},
{"not available errno", wayland.Error{
Cause: wayland.RNotAvail,
Errno: syscall.EAGAIN,
}, "compositor does not implement security_context_v1"},
{"socket", wayland.Error{
Cause: wayland.RSocket,
Errno: stub.UniqueError(4),
}, "unique error 4 injected by the test suite"},
{"socket invalid", wayland.Error{
Cause: wayland.RSocket,
}, "socket operation failed"},
{"invalid", wayland.Error{
Cause: 0xbad,
}, "impossible outcome"},
{"invalid errno", wayland.Error{
Cause: 0xbad,
Errno: stub.UniqueError(5),
}, "impossible outcome: unique error 5 injected by the test suite"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.err.Message(); got != tc.want {
t.Errorf("Message: %q, want %q", got, tc.want)
}
})
}
}