diff --git a/internal/system/output.go b/internal/system/output.go index 71acace..a1666dc 100644 --- a/internal/system/output.go +++ b/internal/system/output.go @@ -40,6 +40,10 @@ func (e *OpError) Error() string { } func (e *OpError) Message() string { + if m, ok := message.GetMessage(e.Err); ok { + return m + } + switch { case e.Msg != "": return e.Error() diff --git a/internal/wayland/wayland-client-helper.c b/internal/wayland/wayland-client-helper.c index b1fcca9..a6a824b 100644 --- a/internal/wayland/wayland-client-helper.c +++ b/internal/wayland/wayland-client-helper.c @@ -31,67 +31,84 @@ static const struct wl_registry_listener registry_listener = { .global_remove = registry_handle_global_remove, }; -int32_t hakurei_bind_wayland_fd( +hakurei_wayland_res hakurei_bind_wayland_fd( char *socket_path, int fd, const char *app_id, const char *instance_id, 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); - if (!display) { - res = 1; + if (display == NULL) { + res = HAKUREI_WAYLAND_CONNECT; goto out; }; - struct wl_registry *registry; registry = wl_display_get_registry(display); - - struct wp_security_context_manager_v1 *security_context_manager = NULL; - wl_registry_add_listener(registry, ®istry_listener, &security_context_manager); - int ret; - ret = wl_display_roundtrip(display); - wl_registry_destroy(registry); - if (ret < 0) + if (wl_registry_add_listener(registry, ®istry_listener, &security_context_manager) < 0) { + res = HAKUREI_WAYLAND_LISTENER; goto out; - - if (!security_context_manager) { - res = 2; + } + event_cnt = wl_display_roundtrip(display); + wl_registry_destroy(registry); + if (event_cnt < 0) { + res = HAKUREI_WAYLAND_ROUNDTRIP; goto out; } - int listen_fd = -1; - listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (listen_fd < 0) + if (security_context_manager == NULL) { + res = HAKUREI_WAYLAND_NOT_AVAIL; 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; 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; + } - if (listen(listen_fd, 0) != 0) + if (listen(listen_fd, 0) != 0) { + res = HAKUREI_WAYLAND_LISTEN; 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); + 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_app_id(security_context, app_id); wp_security_context_v1_set_instance_id(security_context, instance_id); wp_security_context_v1_commit(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; + } out: if (listen_fd >= 0) close(listen_fd); - if (security_context_manager) + if (security_context_manager != NULL) wp_security_context_manager_v1_destroy(security_context_manager); - if (display) + if (display != NULL) wl_display_disconnect(display); free((void *)socket_path); diff --git a/internal/wayland/wayland-client-helper.h b/internal/wayland/wayland-client-helper.h index 6ed93a4..dc456c3 100644 --- a/internal/wayland/wayland-client-helper.h +++ b/internal/wayland/wayland-client-helper.h @@ -1,6 +1,22 @@ -#include +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, int fd, const char *app_id, diff --git a/internal/wayland/wayland.go b/internal/wayland/wayland.go index 7a8694c..a7cddf5 100644 --- a/internal/wayland/wayland.go +++ b/internal/wayland/wayland.go @@ -12,7 +12,6 @@ package wayland */ import "C" import ( - "errors" "strings" "syscall" ) @@ -31,18 +30,96 @@ const ( FallbackName = "wayland-0" ) -var resErr = [...]error{ - 0: nil, - 1: errors.New("wl_display_connect_to_fd() failed"), - 2: errors.New("wp_security_context_v1 not available"), +type ( + // Res is the outcome of a call to hakurei_bind_wayland_fd. + Res = C.hakurei_wayland_res + + // 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 { if hasNull(appID) || hasNull(instanceID) { 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 } diff --git a/internal/wayland/wayland_test.go b/internal/wayland/wayland_test.go new file mode 100644 index 0000000..059ce10 --- /dev/null +++ b/internal/wayland/wayland_test.go @@ -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) + } + }) + } +}