diff options
-rw-r--r-- | common/connection.c | 312 | ||||
-rw-r--r-- | common/drm.c | 23 | ||||
-rw-r--r-- | common/evdev.c | 15 | ||||
-rw-r--r-- | common/list.c | 76 | ||||
-rw-r--r-- | common/log.c | 80 | ||||
-rw-r--r-- | common/terminal.c | 202 | ||||
-rw-r--r-- | examples/simpletest/main.c | 60 | ||||
-rw-r--r-- | include/backend.h | 32 | ||||
-rw-r--r-- | include/client.h | 37 | ||||
-rw-r--r-- | include/compiler.h | 18 | ||||
-rw-r--r-- | include/connection.h | 36 | ||||
-rw-r--r-- | include/drm.h | 10 | ||||
-rw-r--r-- | include/evdev.h | 9 | ||||
-rw-r--r-- | include/libseat.h | 140 | ||||
-rw-r--r-- | include/list.h | 22 | ||||
-rw-r--r-- | include/log.h | 51 | ||||
-rw-r--r-- | include/poller.h | 137 | ||||
-rw-r--r-- | include/protocol.h | 64 | ||||
-rw-r--r-- | include/seat.h | 47 | ||||
-rw-r--r-- | include/server.h | 26 | ||||
-rw-r--r-- | include/terminal.h | 14 | ||||
-rw-r--r-- | libseat/backend/logind.c | 782 | ||||
-rw-r--r-- | libseat/backend/seatd.c | 545 | ||||
-rw-r--r-- | libseat/libseat.c | 111 | ||||
-rw-r--r-- | meson.build | 157 | ||||
-rw-r--r-- | meson_options.txt | 5 | ||||
-rw-r--r-- | seatd/client.c | 484 | ||||
-rw-r--r-- | seatd/poll/basic_poller.c | 386 | ||||
-rw-r--r-- | seatd/poll/poller.c | 53 | ||||
-rw-r--r-- | seatd/seat.c | 521 | ||||
-rw-r--r-- | seatd/seatd.c | 57 | ||||
-rw-r--r-- | seatd/server.c | 232 |
32 files changed, 4744 insertions, 0 deletions
diff --git a/common/connection.c b/common/connection.c new file mode 100644 index 0000000..2545e5d --- /dev/null +++ b/common/connection.c @@ -0,0 +1,312 @@ +#include <errno.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <unistd.h> + +#include "compiler.h" +#include "connection.h" + +#define CLEN (CMSG_LEN(MAX_FDS_OUT * sizeof(int))) + +ALWAYS_INLINE static uint32_t connection_buffer_mask(const uint32_t idx) { + return idx & (CONNECTION_BUFFER_SIZE - 1); +} + +ALWAYS_INLINE static uint32_t connection_buffer_size(const struct connection_buffer *b) { + return b->head - b->tail; +} + +ALWAYS_INLINE static void connection_buffer_consume(struct connection_buffer *b, const size_t size) { + b->tail += size; +} + +ALWAYS_INLINE static void connection_buffer_restore(struct connection_buffer *b, const size_t size) { + b->tail -= size; +} + +/* + * connection_buffer_get_iov prepares I/O vectors pointing to our ring buffer. + * Two may be used if the buffer has wrapped around. + */ +static void connection_buffer_get_iov(struct connection_buffer *b, struct iovec *iov, int *count) { + uint32_t head = connection_buffer_mask(b->head); + uint32_t tail = connection_buffer_mask(b->tail); + if (tail < head) { + iov[0].iov_base = b->data + tail; + iov[0].iov_len = head - tail; + *count = 1; + } else if (head == 0) { + iov[0].iov_base = b->data + tail; + iov[0].iov_len = sizeof b->data - tail; + *count = 1; + } else { + iov[0].iov_base = b->data + tail; + iov[0].iov_len = sizeof b->data - tail; + iov[1].iov_base = b->data; + iov[1].iov_len = head; + *count = 2; + } +} + +/* + * connection_buffer_put_iov prepares I/O vectors pointing to our ring buffer. + * Two may be used if the buffer has wrapped around. + */ +static void connection_buffer_put_iov(struct connection_buffer *b, struct iovec *iov, int *count) { + uint32_t head = connection_buffer_mask(b->head); + uint32_t tail = connection_buffer_mask(b->tail); + if (head < tail) { + iov[0].iov_base = b->data + head; + iov[0].iov_len = tail - head; + *count = 1; + } else if (tail == 0) { + iov[0].iov_base = b->data + head; + iov[0].iov_len = sizeof b->data - head; + *count = 1; + } else { + iov[0].iov_base = b->data + head; + iov[0].iov_len = sizeof b->data - head; + iov[1].iov_base = b->data; + iov[1].iov_len = tail; + *count = 2; + } +} + +/* + * connection_buffer_copy copies from our ring buffer into a linear buffer. + */ +static void connection_buffer_copy(const struct connection_buffer *b, void *data, const size_t count) { + uint32_t tail = connection_buffer_mask(b->tail); + if (tail + count <= sizeof b->data) { + memcpy(data, b->data + tail, count); + return; + } + + uint32_t size = sizeof b->data - tail; + memcpy(data, b->data + tail, size); + memcpy((char *)data + size, b->data, count - size); +} +/* + * connection_buffer_copy copies from a linear buffer into our ring buffer. + */ +static int connection_buffer_put(struct connection_buffer *b, const void *data, const size_t count) { + if (count > sizeof(b->data)) { + errno = EOVERFLOW; + return -1; + } + + uint32_t head = connection_buffer_mask(b->head); + if (head + count <= sizeof b->data) { + memcpy(b->data + head, data, count); + } else { + uint32_t size = sizeof b->data - head; + memcpy(b->data + head, data, size); + memcpy(b->data, (const char *)data + size, count - size); + } + + b->head += count; + return 0; +} + +/* + * close_fds closes all fds within a connection_buffer + */ +static void connection_buffer_close_fds(struct connection_buffer *buffer) { + size_t size = connection_buffer_size(buffer); + if (size == 0) { + return; + } + int fds[sizeof(buffer->data) / sizeof(int)]; + connection_buffer_copy(buffer, fds, size); + int count = size / sizeof fds[0]; + size = count * sizeof fds[0]; + for (int idx = 0; idx < count; idx++) { + close(fds[idx]); + } + connection_buffer_consume(buffer, size); +} + +/* + * build_cmsg prepares a cmsg from a buffer full of fds + */ +static void build_cmsg(struct connection_buffer *buffer, char *data, int *clen) { + size_t size = connection_buffer_size(buffer); + if (size > MAX_FDS_OUT * sizeof(int)) { + size = MAX_FDS_OUT * sizeof(int); + } + + if (size <= 0) { + *clen = 0; + return; + } + + struct cmsghdr *cmsg = (struct cmsghdr *)data; + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(size); + connection_buffer_copy(buffer, CMSG_DATA(cmsg), size); + *clen = cmsg->cmsg_len; +} + +static int decode_cmsg(struct connection_buffer *buffer, struct msghdr *msg) { + bool overflow = false; + struct cmsghdr *cmsg; + for (cmsg = CMSG_FIRSTHDR(msg); cmsg != NULL; cmsg = CMSG_NXTHDR(msg, cmsg)) { + if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS) { + continue; + } + + size_t size = cmsg->cmsg_len - CMSG_LEN(0); + size_t max = sizeof(buffer->data) - connection_buffer_size(buffer); + if (size > max || overflow) { + overflow = true; + size /= sizeof(int); + for (size_t idx = 0; idx < size; idx++) { + close(((int *)CMSG_DATA(cmsg))[idx]); + } + } else if (connection_buffer_put(buffer, CMSG_DATA(cmsg), size) < 0) { + return -1; + } + } + + if (overflow) { + errno = EOVERFLOW; + return -1; + } + return 0; +} + +int connection_read(struct connection *connection) { + if (connection_buffer_size(&connection->in) >= sizeof(connection->in.data)) { + errno = EOVERFLOW; + return -1; + } + + int count; + struct iovec iov[2]; + connection_buffer_put_iov(&connection->in, iov, &count); + + char cmsg[CLEN]; + struct msghdr msg = { + .msg_name = NULL, + .msg_namelen = 0, + .msg_iov = iov, + .msg_iovlen = count, + .msg_control = cmsg, + .msg_controllen = sizeof cmsg, + .msg_flags = 0, + }; + + int len; + do { + len = recvmsg(connection->fd, &msg, MSG_DONTWAIT | MSG_CMSG_CLOEXEC); + if (len == -1 && errno != EINTR) + return -1; + } while (len == -1); + + if (decode_cmsg(&connection->fds_in, &msg) != 0) { + return -1; + } + connection->in.head += len; + + return connection_buffer_size(&connection->in); +} + +int connection_flush(struct connection *connection) { + if (!connection->want_flush) { + return 0; + } + + uint32_t tail = connection->out.tail; + while (connection->out.head - connection->out.tail > 0) { + int count; + struct iovec iov[2]; + connection_buffer_get_iov(&connection->out, iov, &count); + + int clen; + char cmsg[CLEN]; + build_cmsg(&connection->fds_out, cmsg, &clen); + struct msghdr msg = { + .msg_name = NULL, + .msg_namelen = 0, + .msg_iov = iov, + .msg_iovlen = count, + .msg_control = (clen > 0) ? cmsg : NULL, + .msg_controllen = clen, + .msg_flags = 0, + }; + + int len; + do { + len = sendmsg(connection->fd, &msg, MSG_NOSIGNAL | MSG_DONTWAIT); + if (len == -1 && errno != EINTR) + return -1; + } while (len == -1); + connection_buffer_close_fds(&connection->fds_out); + connection->out.tail += len; + } + connection->want_flush = 0; + return connection->out.head - tail; +} + +int connection_put(struct connection *connection, const void *data, size_t count) { + if (connection_buffer_size(&connection->out) + count > CONNECTION_BUFFER_SIZE) { + connection->want_flush = 1; + if (connection_flush(connection) == -1) { + return -1; + } + } + + if (connection_buffer_put(&connection->out, data, count) == -1) { + return -1; + } + + connection->want_flush = 1; + return 0; +} + +int connection_put_fd(struct connection *connection, int fd) { + if (connection_buffer_size(&connection->fds_out) == MAX_FDS_OUT * sizeof fd) { + errno = EOVERFLOW; + return -1; + } + + return connection_buffer_put(&connection->fds_out, &fd, sizeof fd); +} + +int connection_get(struct connection *connection, void *dst, size_t count) { + if (count > connection_buffer_size(&connection->in)) { + errno = EAGAIN; + return -1; + } + connection_buffer_copy(&connection->in, dst, count); + connection_buffer_consume(&connection->in, count); + return count; +} + +int connection_get_fd(struct connection *connection) { + int fd; + if (sizeof fd > connection_buffer_size(&connection->fds_in)) { + errno = EAGAIN; + return -1; + } + connection_buffer_copy(&connection->fds_in, &fd, sizeof fd); + connection_buffer_consume(&connection->fds_in, sizeof fd); + return fd; +} + +void connection_close_fds(struct connection *connection) { + connection_buffer_close_fds(&connection->fds_in); + connection_buffer_close_fds(&connection->fds_out); +} + +size_t connection_pending(struct connection *connection) { + return connection_buffer_size(&connection->in); +} + +void connection_restore(struct connection *connection, size_t count) { + connection_buffer_restore(&connection->in, count); +} diff --git a/common/drm.c b/common/drm.c new file mode 100644 index 0000000..4b70b64 --- /dev/null +++ b/common/drm.c @@ -0,0 +1,23 @@ +#include <sys/ioctl.h> +#include <sys/sysmacros.h> +#include <sys/types.h> + +#include "drm.h" + +// From libdrm +#define DRM_IOCTL_BASE 'd' +#define DRM_IO(nr) _IO(DRM_IOCTL_BASE, nr) +#define DRM_IOCTL_SET_MASTER DRM_IO(0x1e) +#define DRM_IOCTL_DROP_MASTER DRM_IO(0x1f) + +int drm_set_master(int fd) { + return ioctl(fd, DRM_IOCTL_SET_MASTER, 0); +} + +int drm_drop_master(int fd) { + return ioctl(fd, DRM_IOCTL_DROP_MASTER, 0); +} + +int dev_is_drm(dev_t device) { + return major(device) == 226; +} diff --git a/common/evdev.c b/common/evdev.c new file mode 100644 index 0000000..fe6922d --- /dev/null +++ b/common/evdev.c @@ -0,0 +1,15 @@ +#include <linux/input.h> +#include <stdlib.h> +#include <sys/ioctl.h> +#include <sys/sysmacros.h> +#include <sys/types.h> + +#include "evdev.h" + +int evdev_revoke(int fd) { + return ioctl(fd, EVIOCREVOKE, NULL); +} + +int dev_is_evdev(dev_t device) { + return major(device) == 13; +} diff --git a/common/list.c b/common/list.c new file mode 100644 index 0000000..1a86837 --- /dev/null +++ b/common/list.c @@ -0,0 +1,76 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "list.h" + +void list_init(struct list *list) { + list->capacity = 10; + list->length = 0; + list->items = malloc(sizeof(void *) * list->capacity); +} + +static void list_resize(struct list *list) { + if (list->length == list->capacity) { + list->capacity *= 2; + list->items = realloc(list->items, sizeof(void *) * list->capacity); + } +} + +void list_free(struct list *list) { + list->capacity = 0; + list->length = 0; + free(list->items); +} + +void list_add(struct list *list, void *item) { + list_resize(list); + list->items[list->length++] = item; +} + +void list_insert(struct list *list, size_t index, void *item) { + list_resize(list); + memmove(&list->items[index + 1], &list->items[index], + sizeof(void *) * (list->length - index)); + list->length++; + list->items[index] = item; +} + +void list_del(struct list *list, size_t index) { + list->length--; + memmove(&list->items[index], &list->items[index + 1], + sizeof(void *) * (list->length - index)); +} + +size_t list_find(struct list *list, const void *item) { + for (size_t i = 0; i < list->length; i++) { + if (list->items[i] == item) { + return i; + } + } + return -1; +} + +void list_concat(struct list *list, struct list *source) { + if (list->length + source->length > list->capacity) { + while (list->length + source->length > list->capacity) { + list->capacity *= 2; + } + list->items = realloc(list->items, sizeof(void *) * list->capacity); + } + memmove(&list->items[list->length], source->items, sizeof(void *) * (source->length)); + list->length += source->length; +} + +void list_truncate(struct list *list) { + list->length = 0; +} + +void *list_pop_front(struct list *list) { + if (list->length == 0) { + return NULL; + } + void *item = list->items[0]; + list_del(list, 0); + return item; +} diff --git a/common/log.c b/common/log.c new file mode 100644 index 0000000..1474dcc --- /dev/null +++ b/common/log.c @@ -0,0 +1,80 @@ +#include <errno.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <time.h> +#include <unistd.h> + +#include "log.h" + +const long NSEC_PER_SEC = 1000000000; + +static enum libseat_log_level current_log_level; +static struct timespec start_time = {-1, -1}; +static bool colored = false; + +static const char *verbosity_colors[] = { + [LIBSEAT_SILENT] = "", + [LIBSEAT_ERROR] = "\x1B[1;31m", + [LIBSEAT_INFO] = "\x1B[1;34m", + [LIBSEAT_DEBUG] = "\x1B[1;90m", +}; + +static const char *verbosity_headers[] = { + [LIBSEAT_SILENT] = "", + [LIBSEAT_ERROR] = "[ERROR]", + [LIBSEAT_INFO] = "[INFO]", + [LIBSEAT_DEBUG] = "[DEBUG]", +}; + +static void timespec_sub(struct timespec *r, const struct timespec *a, const struct timespec *b) { + r->tv_sec = a->tv_sec - b->tv_sec; + r->tv_nsec = a->tv_nsec - b->tv_nsec; + if (r->tv_nsec < 0) { + r->tv_sec--; + r->tv_nsec += NSEC_PER_SEC; + } +} + +void libseat_log_init(enum libseat_log_level level) { + if (start_time.tv_sec >= 0) { + return; + } + clock_gettime(CLOCK_MONOTONIC, &start_time); + current_log_level = level; + colored = isatty(STDERR_FILENO); +} + +void _libseat_logf(enum libseat_log_level level, const char *fmt, ...) { + int stored_errno = errno; + va_list args; + if (level < current_log_level) { + return; + } + + struct timespec ts = {0}; + clock_gettime(CLOCK_MONOTONIC, &ts); + timespec_sub(&ts, &ts, &start_time); + unsigned c = (level < LIBSEAT_LOG_LEVEL_LAST) ? level : LIBSEAT_LOG_LEVEL_LAST - 1; + + const char *prefix, *postfix; + + if (colored) { + prefix = verbosity_colors[c]; + postfix = "\x1B[0m\n"; + } else { + prefix = verbosity_headers[c]; + postfix = "\n"; + } + + fprintf(stderr, "%02d:%02d:%02d.%03ld %s ", (int)(ts.tv_sec / 60 / 60), + (int)(ts.tv_sec / 60 % 60), (int)(ts.tv_sec % 60), ts.tv_nsec / 1000000, prefix); + + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); + + fprintf(stderr, "%s", postfix); + errno = stored_errno; +} diff --git a/common/terminal.c b/common/terminal.c new file mode 100644 index 0000000..c9ee758 --- /dev/null +++ b/common/terminal.c @@ -0,0 +1,202 @@ +#include "string.h" +#include <errno.h> +#include <fcntl.h> +#include <linux/kd.h> +#include <linux/vt.h> +#include <signal.h> +#include <stdbool.h> +#include <stdio.h> +#include <sys/ioctl.h> +#include <unistd.h> + +#include "log.h" +#include "terminal.h" + +#define TTYPATHLEN 64 + +int terminal_current_vt(void) { + struct vt_stat st; + int fd = open("/dev/tty0", O_RDWR | O_NOCTTY); + if (fd == -1) { + log_errorf("could not open tty0: %s", strerror(errno)); + return -1; + } + int res = ioctl(fd, VT_GETSTATE, &st); + close(fd); + if (res == -1) { + log_errorf("could not retrieve VT state: %s", strerror(errno)); + return -1; + } + return st.v_active; +} + +int terminal_setup(int vt) { + log_debugf("setting up vt %d", vt); + if (vt == -1) { + vt = 0; + } + char path[TTYPATHLEN]; + if (snprintf(path, TTYPATHLEN, "/dev/tty%d", vt) == -1) { + log_errorf("could not generate tty path: %s", strerror(errno)); + return -1; + } + int fd = open(path, O_RDWR | O_NOCTTY); + if (fd == -1) { + log_errorf("could not open target tty: %s", strerror(errno)); + return -1; + } + + static struct vt_mode mode = { + .mode = VT_PROCESS, + .waitv = 0, + .relsig = SIGUSR1, + .acqsig = SIGUSR2, + .frsig = 0, + }; + + int res = ioctl(fd, VT_SETMODE, &mode); + close(fd); + if (res == -1) { + log_errorf("could not set VT mode: %s", strerror(errno)); + } + + return res; +} + +int terminal_teardown(int vt) { + log_debugf("tearing down vt %d", vt); + if (vt == -1) { + vt = 0; + } + char path[TTYPATHLEN]; + if (snprintf(path, TTYPATHLEN, "/dev/tty%d", vt) == -1) { + log_errorf("could not generate tty path: %s", strerror(errno)); + return -1; + } + int fd = open(path, O_RDWR | O_NOCTTY); + if (fd == -1) { + log_errorf("could not open target tty: %s", strerror(errno)); + return -1; + } + if (ioctl(fd, KDSETMODE, KD_TEXT) == -1) { + log_errorf("could not set KD graphics mode: %s", strerror(errno)); + close(fd); + return -1; + } + if (ioctl(fd, KDSKBMODE, K_UNICODE) == -1) { + log_errorf("could not set KD keyboard mode: %s", strerror(errno)); + close(fd); + return -1; + } + + static struct vt_mode mode = { + .mode = VT_PROCESS, + .waitv = 0, + .relsig = SIGUSR1, + .acqsig = SIGUSR2, + .frsig = 0, + }; + if (ioctl(fd, VT_SETMODE, &mode) == -1) { + log_errorf("could not set VT mode: %s", strerror(errno)); + close(fd); + return -1; + } + + close(fd); + return 0; +} + +int terminal_switch_vt(int vt) { + log_debugf("switching to vt %d", vt); + int fd = open("/dev/tty0", O_RDWR | O_NOCTTY); + if (fd == -1) { + log_errorf("could not open tty0: %s", strerror(errno)); + return -1; + } + + static struct vt_mode mode = { + .mode = VT_PROCESS, + .waitv = 0, + .relsig = SIGUSR1, + .acqsig = SIGUSR2, + .frsig = 0, + }; + + if (ioctl(fd, VT_SETMODE, &mode) == -1) { + log_errorf("could not set VT mode: %s", strerror(errno)); + close(fd); + return -1; + } + + if (ioctl(fd, VT_ACTIVATE, vt) == -1) { + log_errorf("could not activate VT: %s", strerror(errno)); + close(fd); + return -1; + } + + close(fd); + return 0; +} + +int terminal_ack_switch(void) { + log_debug("acking vt switch"); + int fd = open("/dev/tty0", O_RDWR | O_NOCTTY); + if (fd == -1) { + log_errorf("could not open tty0: %s", strerror(errno)); + return -1; + } + + int res = ioctl(fd, VT_RELDISP, VT_ACKACQ); + close(fd); + if (res == -1) { + log_errorf("could not ack VT switch: %s", strerror(errno)); + } + + return res; +} + +int terminal_set_keyboard(int vt, bool enable) { + log_debugf("setting KD keyboard state to %d on vt %d", enable, vt); + if (vt == -1) { + vt = 0; + } + char path[TTYPATHLEN]; + if (snprintf(path, TTYPATHLEN, "/dev/tty%d", vt) == -1) { + log_errorf("could not generate tty path: %s", strerror(errno)); + return -1; + } + int fd = open(path, O_RDWR | O_NOCTTY); + if (fd == -1) { + log_errorf("could not generate tty path: %s", strerror(errno)); + return -1; + } + int res = ioctl(fd, KDSKBMODE, enable ? K_UNICODE : K_OFF); + close(fd); + if (res == -1) { + log_errorf("could not set KD keyboard mode: %s", strerror(errno)); + } + return res; +} + +int terminal_set_graphics(int vt, bool enable) { + log_debugf("setting KD graphics state to %d on vt %d", enable, vt); + if (vt == -1) { + vt = 0; + } + char path[TTYPATHLEN]; + if (snprintf(path, TTYPATHLEN, "/dev/tty%d", vt) == -1) { + log_errorf("could not generate tty path: %s", strerror(errno)); + return -1; + } + int fd = open(path, O_RDWR | O_NOCTTY); + if (fd == -1) { + log_errorf("could not generate tty path: %s", strerror(errno)); + return -1; + } + int res = ioctl(fd, KDSETMODE, enable ? KD_GRAPHICS : KD_TEXT); + close(fd); + if (res == -1) { + log_errorf("could not set KD graphics mode: %s", strerror(errno)); + } + return res; +} diff --git a/examples/simpletest/main.c b/examples/simpletest/main.c new file mode 100644 index 0000000..cd2e243 --- /dev/null +++ b/examples/simpletest/main.c @@ -0,0 +1,60 @@ +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "libseat.h" + +static void handle_enable(struct libseat *backend, void *data) { + (void)backend; + int *active = (int *)data; + (*active)++; +} + +static void handle_disable(struct libseat *backend, void *data) { + (void)backend; + int *active = (int *)data; + (*active)--; + + libseat_disable_seat(backend); +} + +int main(int argc, char *argv[]) { + (void)argc; + (void)argv; + + int active = 0; + struct libseat_seat_listener listener = { + .enable_seat = handle_enable, + .disable_seat = handle_disable, + }; + struct libseat *backend = libseat_open_seat(&listener, &active); + fprintf(stderr, "libseat_open_seat(listener: %p, userdata: %p) = %p\n", (void *)&listener, + (void *)&active, (void *)backend); + if (backend == NULL) { + fprintf(stderr, "libseat_open_seat() failed: %s\n", strerror(errno)); + return -1; + } + + while (active == 0) { + fprintf(stderr, "waiting for activation...\n"); + libseat_dispatch(backend, -1); + } + fprintf(stderr, "active!\n"); + + int fd, device; + device = libseat_open_device(backend, "/dev/dri/card0", &fd); + fprintf(stderr, "libseat_open_device(backend: %p, path: %s, fd: %p) = %d\n", + (void *)backend, "/dev/dri/card0", (void *)&fd, device); + if (device == -1) { + fprintf(stderr, "libseat_open_device() failed: %s\n", strerror(errno)); + libseat_close_seat(backend); + return 1; + } + + close(fd); + libseat_close_device(backend, device); + libseat_close_seat(backend); + return 0; +} diff --git a/include/backend.h b/include/backend.h new file mode 100644 index 0000000..e310a49 --- /dev/null +++ b/include/backend.h @@ -0,0 +1,32 @@ +#ifndef _SEATD_BACKEND_H +#define _SEATD_BACKEND_H + +#include "libseat.h" + +struct libseat_impl; +struct libseat_seat_listener; + +struct libseat { + const struct libseat_impl *impl; +}; + +struct named_backend { + const char *name; + const struct libseat_impl *backend; +}; + +struct libseat_impl { + struct libseat *(*open_seat)(struct libseat_seat_listener *listener, void *data); + int (*disable_seat)(struct libseat *seat); + int (*close_seat)(struct libseat *seat); + const char *(*seat_name)(struct libseat *seat); + + int (*open_device)(struct libseat *seat, const char *path, int *fd); + int (*close_device)(struct libseat *seat, int device_id); + int (*switch_session)(struct libseat *seat, int session); + + int (*get_fd)(struct libseat *seat); + int (*dispatch)(struct libseat *seat, int timeout); +}; + +#endif diff --git a/include/client.h b/include/client.h new file mode 100644 index 0000000..6084980 --- /dev/null +++ b/include/client.h @@ -0,0 +1,37 @@ +#ifndef _SEATD_CLIENT_H +#define _SEATD_CLIENT_H + +#include <stdint.h> +#include <sys/types.h> +#include <unistd.h> + +#include "connection.h" +#include "list.h" + +struct server; + +struct client { + struct server *server; + struct event_source_fd *event_source; + struct connection connection; + + pid_t pid; + uid_t uid; + gid_t gid; + + struct seat *seat; + int seat_vt; + + struct list devices; +}; + +struct client *client_create(struct server *server, int client_fd); +void client_kill(struct client *client); +void client_destroy(struct client *client); + +int client_handle_connection(int fd, uint32_t mask, void *data); +int client_get_session(struct client *client); +int client_enable_seat(struct client *client); +int client_disable_seat(struct client *client); + +#endif diff --git a/include/compiler.h b/include/compiler.h new file mode 100644 index 0000000..d0f6ed2 --- /dev/null +++ b/include/compiler.h @@ -0,0 +1,18 @@ +#ifndef _VISIBILITY_H +#define _VISIBILITY_H + +#ifdef __GNUC__ +#define ATTRIB_PRINTF(start, end) __attribute__((format(printf, start, end))) +#else +#define ATTRIB_PRINTF(start, end) +#endif + +#if defined(__GNUC__) && __GNUC__ >= 4 +#define LIBSEAT_EXPORT __attribute__((visibility("default"))) +#define ALWAYS_INLINE __attribute__((always_inline)) inline +#else +#define LIBSEAT_EXPORT +#define ALWAYS_INLINE inline +#endif + +#endif diff --git a/include/connection.h b/include/connection.h new file mode 100644 index 0000000..6c57901 --- /dev/null +++ b/include/connection.h @@ -0,0 +1,36 @@ +#ifndef _SEATD_CONNECTION_H +#define _SEATD_CONNECTION_H +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +#define CONNECTION_BUFFER_SIZE 1024 + +#define MAX_FDS_OUT 8 + +struct connection_buffer { + uint32_t head, tail; + char data[CONNECTION_BUFFER_SIZE]; +}; + +struct connection { + struct connection_buffer in, out; + struct connection_buffer fds_in, fds_out; + int fd; + bool want_flush; +}; + +int connection_read(struct connection *connection); +int connection_flush(struct connection *connection); + +int connection_put(struct connection *connection, const void *data, size_t count); +int connection_put_fd(struct connection *connection, int fd); + +size_t connection_pending(struct connection *connection); +int connection_get(struct connection *connection, void *dst, size_t count); +int connection_get_fd(struct connection *connection); +void connection_restore(struct connection *connection, size_t count); + +void connection_close_fds(struct connection *connection); + +#endif diff --git a/include/drm.h b/include/drm.h new file mode 100644 index 0000000..1012c89 --- /dev/null +++ b/include/drm.h @@ -0,0 +1,10 @@ +#ifndef _SEATD_DRM_H +#define _SEATD_DRM_H + +#include <sys/types.h> + +int drm_set_master(int fd); +int drm_drop_master(int fd); +int dev_is_drm(dev_t device); + +#endif diff --git a/include/evdev.h b/include/evdev.h new file mode 100644 index 0000000..0f20e3e --- /dev/null +++ b/include/evdev.h @@ -0,0 +1,9 @@ +#ifndef _SEATD_EVDEV_H +#define _SEATD_EVDEV_H + +#include <sys/types.h> + +int evdev_revoke(int fd); +int dev_is_evdev(dev_t device); + +#endif diff --git a/include/libseat.h b/include/libseat.h new file mode 100644 index 0000000..7e9ef1f --- /dev/null +++ b/include/libseat.h @@ -0,0 +1,140 @@ +#ifndef _LIBSEAT_H +#define _LIBSEAT_H + +#include <stdarg.h> + +/* + * An opaque struct containing an opened seat, created by libseat_open-seat and + * destroyed by libseat_close_seat. + */ +struct libseat; + +/* + * A seat event listener, given to libseat_open_seat. + */ +struct libseat_seat_listener { + /* + * The seat has been enabled, and is now valid for use. Re-open all seat + * devices to ensure that they are operational, as existing fds may have + * had their functionality blocked or revoked. + * + * Must be non-NULL. + */ + void (*enable_seat)(struct libseat *seat, void *userdata); + + /* + * The seat has been disabled. This event signals that the application + * is going to lose its seat access. The event *must* be acknowledged + * with libseat_disable_seat shortly after receiving this event. + * + * If the recepient fails to acknowledge the event in time, seat devices + * may be forcibly revoked by the seat provider. + * + * Must be non-NULL. + */ + void (*disable_seat)(struct libseat *seat, void *userdata); +}; + +/* + * Opens a seat, taking control of it if possible and returning a pointer to + * the libseat instance. If LIBSEAT_BACKEND is set, the specified backend is + * used. Otherwise, the first successful backend will be used. + * + * The seat listener specified is used to signal events on the seat, and must + * be non-NULL. The userdata pointer will be provided in all calls to the seat + * listener. + * + * The available backends, if enabled at compile-time, are: seatd, logind and + * builtin. + * + * To use builtin, the process must have CAP_SYS_ADMIN or be root at the time + * of the call. These privileges can be dropped at any point after the call. + * + * The returned pointer must be destroyed with libseat_close_seat. + * + * Returns a pointer to an opaque libseat struct on success. Returns NULL and + * sets errno on error. + */ +struct libseat *libseat_open_seat(struct libseat_seat_listener *listener, void *userdata); + +/* + * Disables a seat, used in response to a disable_seat event. After disabling + * the seat, the seat devices must not be used until enable_seat is received, + * and all requests on the seat will fail during this period. + * + * Returns 0 on success. -1 and sets errno on error. + */ +int libseat_disable_seat(struct libseat *seat); + +/* + * Closes the seat. This frees the libseat structure. + * + * Returns 0 on success. Returns -1 and sets errno on error. + */ +int libseat_close_seat(struct libseat *seat); + +/* + * Opens a device on the seat, returning its device ID and placing the fd in + * the specified pointer. + * + * This will only succeed if the seat is active and the device is of a type + * permitted for opening on the backend, such as drm and evdev. + * + * The device may be revoked in some situations, such as in situations where a + * seat session switch is being forced. + * + * Returns the device id on success. Returns -1 and sets errno on error. + */ +int libseat_open_device(struct libseat *seat, const char *path, int *fd); + +/* + * Closes a device that has been opened on the seat using the device_id from + * libseat_open_device. + * + * Returns 0 on success. Returns -1 and sets errno on error. + */ +int libseat_close_device(struct libseat *seat, int device_id); + +/* + * Retrieves the name of the seat that is currently made available through the + * provided libseat instance. + * + * The returned string is owned by the libseat instance, and must not be + * modified. It remains valid as long as the seat is open. + */ +const char *libseat_seat_name(struct libseat *seat); + +/* + * Requests that the seat switches session to the specified session number. + * For seats that are VT-bound, the session number matches the VT number, and + * switching session results in a VT switch. + * + * A call to libseat_switch_session does not imply that a switch will occur, + * and the caller should assume that the session continues unaffected. + * + * Returns 0 on success. Returns -1 and sets errno on error. + */ +int libseat_switch_session(struct libseat *seat, int session); + +/* + * Retrieve the pollable connection fd for a given libseat instance. Used to + * poll the libseat connection for events that need to be dispatched. + * + * Returns a pollable fd on success. Returns -1 and sets errno on error. + */ +int libseat_get_fd(struct libseat *seat); + +/* + * Reads and dispatches events on the libseat connection fd. + * + * The specified timeout dictates how long libseat might wait for data if none + * is available: 0 means that no wait will occur, -1 means that libseat might + * wait indefinitely for data to arrive, while > 0 is the maximum wait in + * milliseconds that might occur. + * + * Returns a positive number signifying processed internal messages on success. + * Returns 0-if no messages were processed. Returns -1 and sets errno on error. + */ +int libseat_dispatch(struct libseat *seat, int timeout); + +#endif diff --git a/include/list.h b/include/list.h new file mode 100644 index 0000000..3f2ac6c --- /dev/null +++ b/include/list.h @@ -0,0 +1,22 @@ +#ifndef _SEATD_LIST_H +#define _SEATD_LIST_H + +#include <stddef.h> + +struct list { + size_t capacity; + size_t length; + void **items; +}; + +void list_init(struct list *); +void list_free(struct list *list); +void list_add(struct list *list, void *item); +void list_insert(struct list *list, size_t index, void *item); +void list_del(struct list *list, size_t index); +void list_concat(struct list *list, struct list *source); +void list_truncate(struct list *list); +void *list_pop_front(struct list *list); +size_t list_find(struct list *list, const void *item); + +#endif diff --git a/include/log.h b/include/log.h new file mode 100644 index 0000000..18a573e --- /dev/null +++ b/include/log.h @@ -0,0 +1,51 @@ +#ifndef _LOG_H +#define _LOG_H + +#include "compiler.h" +#include <stdarg.h> + +enum libseat_log_level { + LIBSEAT_SILENT = 0, + LIBSEAT_ERROR = 1, + LIBSEAT_INFO = 2, + LIBSEAT_DEBUG = 3, + LIBSEAT_LOG_LEVEL_LAST, +}; + +void libseat_log_init(enum libseat_log_level level); + +void _libseat_logf(enum libseat_log_level level, const char *fmt, ...) ATTRIB_PRINTF(2, 3); + +#ifdef LIBSEAT_REL_SRC_DIR +#define _LIBSEAT_FILENAME ((const char *)__FILE__ + sizeof(LIBSEAT_REL_SRC_DIR) - 1) +#else +#define _LIBSEAT_FILENAME __FILE__ +#endif + +#define log_infof(fmt, ...) \ + _libseat_logf(LIBSEAT_INFO, "[%s:%d] %s: " fmt, _LIBSEAT_FILENAME, __LINE__, __func__, \ + ##__VA_ARGS__) + +#define log_info(str) \ + _libseat_logf(LIBSEAT_INFO, "[%s:%d] %s: %s", _LIBSEAT_FILENAME, __LINE__, __func__, str) + +#define log_errorf(fmt, ...) \ + _libseat_logf(LIBSEAT_ERROR, "[%s:%d] %s: " fmt, _LIBSEAT_FILENAME, __LINE__, __func__, \ + ##__VA_ARGS__) + +#define log_error(str) \ + _libseat_logf(LIBSEAT_ERROR, "[%s:%d] %s: %s", _LIBSEAT_FILENAME, __LINE__, __func__, str) + +#ifdef DEBUG +#define log_debugf(fmt, ...) \ + _libseat_logf(LIBSEAT_DEBUG, "[%s:%d] %s: " fmt, _LIBSEAT_FILENAME, __LINE__, __func__, \ + ##__VA_ARGS__) + +#define log_debug(str) \ + _libseat_logf(LIBSEAT_DEBUG, "[%s:%d] %s: %s", _LIBSEAT_FILENAME, __LINE__, __func__, str) +#else +#define log_debugf(fmt, ...) +#define log_debug(str) +#endif + +#endif diff --git a/include/poller.h b/include/poller.h new file mode 100644 index 0000000..f867df9 --- /dev/null +++ b/include/poller.h @@ -0,0 +1,137 @@ +#ifndef _SEATD_POLLER_H +#define _SEATD_POLLER_H + +#include <stdbool.h> +#include <stdint.h> + +struct poller; +struct event_source_fd; +struct event_source_signal; + +/* + * These are the event types available from the poller. + */ +#define EVENT_READABLE 0x1 +#define EVENT_WRITABLE 0x4 +#define EVENT_ERROR 0x8 +#define EVENT_HANGUP 0x10 + +/** + * The callback type used by event_source_fd, passed to poller_add_fd. + */ +typedef int (*event_source_fd_func_t)(int fd, uint32_t mask, void *data); + +/** + * The interface that an event_source_fd must implement. + */ +struct event_source_fd_impl { + int (*update)(struct event_source_fd *event_source, uint32_t mask); + int (*destroy)(struct event_source_fd *event_source); +}; + +/** + * The fd poller base class. This must be created by poller_add_fd. + */ +struct event_source_fd { + const struct event_source_fd_impl *impl; + event_source_fd_func_t func; + + int fd; + uint32_t mask; + void *data; +}; + +/** + * Removes the event_source_fd from the poller and frees the structure. + */ +int event_source_fd_destroy(struct event_source_fd *event_source); + +/** + * Updates the poll mask applied to this fd, effective on the next poll. + */ +int event_source_fd_update(struct event_source_fd *event_source, uint32_t mask); + +/** + * The callback type used by event_source_signal, passed to poller_add_signal. + */ +typedef int (*event_source_signal_func_t)(int signal, void *data); + +/** + * The interface that an event_source_signal must implement. + */ +struct event_source_signal_impl { + int (*destroy)(struct event_source_signal *event_source); +}; + +/* + * The signal poller base class. This must be created by poller_add_signal. + */ +struct event_source_signal { + const struct event_source_signal_impl *impl; + event_source_signal_func_t func; + + int signal; + void *data; +}; + +/** + * Removes the event_source_siganl from the poller and frees the structure. + */ +int event_source_signal_destroy(struct event_source_signal *event_source); + +/** + * The interface that a poll backend must implement. + */ +struct poll_impl { + struct poller *(*create)(void); + int (*destroy)(struct poller *); + + struct event_source_fd *(*add_fd)(struct poller *, int fd, uint32_t mask, + event_source_fd_func_t func, void *data); + struct event_source_signal *(*add_signal)(struct poller *, int signal, + event_source_signal_func_t func, void *data); + + int (*poll)(struct poller *); +}; + +/** + * The poller base class. This must be created by poller_create. + */ +struct poller { + const struct poll_impl *impl; +}; + +/** + * Creates a poller with the best available polling backend. This poller must + * be torn down with poller_destroy when it is no longer needed. + */ +struct poller *poller_create(void); + +/** + * Destroys the poller. This destroys all remaining event sources, tears down + * the poller and frees the structure. + */ +int poller_destroy(struct poller *poller); + +/** + * Create an fd event source with the provided initial parameters. This event + * source must be torn down with event_source_fd_destroy when it is no longer + * needed. + */ +struct event_source_fd *poller_add_fd(struct poller *poller, int fd, uint32_t mask, + event_source_fd_func_t func, void *data); + +/** + * Create signal event source with the provided initial parameters. This event + * source must be torn down with event_source_signal_destroy when it is no + * longer needed. + */ +struct event_source_signal *poller_add_signal(struct poller *poller, int signal, + event_source_signal_func_t func, void *data); + +/** + * Poll the poller. I don't know what you were expecting. + */ +int poller_poll(struct poller *poller); + +#endif diff --git a/include/protocol.h b/include/protocol.h new file mode 100644 index 0000000..7444b85 --- /dev/null +++ b/include/protocol.h @@ -0,0 +1,64 @@ +#ifndef _SEATD_CONSTANTS_H +#define _SEATD_CONSTANTS_H + +#define MAX_PATH_LEN 256 +#define MAX_SEAT_LEN 64 +#define MAX_SEAT_DEVICES 128 +#define MAX_SESSION_LEN 64 + +#define CLIENT_EVENT(opcode) (opcode) +#define SERVER_EVENT(opcode) ((opcode) + (1 << 15)) + +#define CLIENT_OPEN_SEAT CLIENT_EVENT(1) +#define CLIENT_CLOSE_SEAT CLIENT_EVENT(2) +#define CLIENT_OPEN_DEVICE CLIENT_EVENT(3) +#define CLIENT_CLOSE_DEVICE CLIENT_EVENT(4) +#define CLIENT_DISABLE_SEAT CLIENT_EVENT(5) +#define CLIENT_SWITCH_SESSION CLIENT_EVENT(6) + +#define SERVER_SEAT_OPENED SERVER_EVENT(1) +#define SERVER_SEAT_CLOSED SERVER_EVENT(2) +#define SERVER_DEVICE_OPENED SERVER_EVENT(3) +#define SERVER_DEVICE_CLOSED SERVER_EVENT(4) +#define SERVER_DISABLE_SEAT SERVER_EVENT(5) +#define SERVER_ENABLE_SEAT SERVER_EVENT(6) +#define SERVER_ERROR SERVER_EVENT(0x7FFF) + +#include <stdint.h> + +struct proto_header { + uint16_t opcode; + uint16_t size; +}; + +struct proto_client_open_device { + uint16_t path_len; + // NULL-terminated byte-sequence path_len long follows +}; + +struct proto_client_close_device { + int device_id; +}; + +struct proto_client_switch_session { + int session; +}; + +struct proto_server_seat_opened { + uint16_t seat_name_len; + // NULL-terminated byte-sequence seat_name_len long follows +}; + +struct proto_server_device_opened { + int device_id; +}; + +struct proto_server_device_closed { + int device_id; +}; + +struct proto_server_error { + int error_code; +}; + +#endif diff --git a/include/seat.h b/include/seat.h new file mode 100644 index 0000000..e52a961 --- /dev/null +++ b/include/seat.h @@ -0,0 +1,47 @@ +#ifndef _SEATD_SEAT_H +#define _SEATD_SEAT_H + +#include "list.h" +#include <stdbool.h> +#include <stdint.h> +#include <sys/types.h> + +struct client; + +struct seat_device { + int device_id; + int fd; + int ref_cnt; + bool active; + char *path; + dev_t dev; +}; + +struct seat { + char *seat_name; + struct list clients; + struct client *active_client; + struct client *next_client; + + bool vt_bound; + bool vt_pending_ack; + int next_vt; +}; + +struct seat *seat_create(const char *name, bool vt_bound); +void seat_destroy(struct seat *seat); + +int seat_add_client(struct seat *seat, struct client *client); +int seat_remove_client(struct seat *seat, struct client *client); +int seat_open_client(struct seat *seat, struct client *client); +int seat_close_client(struct seat *seat, struct client *client); + +struct seat_device *seat_open_device(struct client *client, const char *path); +int seat_close_device(struct client *client, struct seat_device *seat_device); +struct seat_device *seat_find_device(struct client *client, int device_id); + +int seat_set_next_session(struct seat *seat, int session); +int seat_activate(struct seat *seat); +int seat_prepare_vt_switch(struct seat *seat); + +#endif diff --git a/include/server.h b/include/server.h new file mode 100644 index 0000000..11de2c5 --- /dev/null +++ b/include/server.h @@ -0,0 +1,26 @@ +#ifndef _SEATD_SERVER_H +#define _SEATD_SERVER_H + +#include <stdbool.h> + +#include "list.h" + +struct poller; +struct client; + +struct server { + bool running; + struct poller *poller; + + struct list seats; +}; + +struct server *server_create(void); +void server_destroy(struct server *server); + +struct seat *server_get_seat(struct server *server, const char *seat_name); + +int server_listen(struct server *server, const char *path); +int server_add_client(struct server *server, int fd); + +#endif diff --git a/include/terminal.h b/include/terminal.h new file mode 100644 index 0000000..c2a49ff --- /dev/null +++ b/include/terminal.h @@ -0,0 +1,14 @@ +#ifndef _SEATD_TERMINAL_H +#define _SEATD_TERMINAL_H + +#include <stdbool.h> + +int terminal_setup(int vt); +int terminal_teardown(int vt); +int terminal_current_vt(void); +int terminal_switch_vt(int vt); +int terminal_ack_switch(void); +int terminal_set_keyboard(int vt, bool enable); +int terminal_set_graphics(int vt, bool enable); + +#endif diff --git a/libseat/backend/logind.c b/libseat/backend/logind.c new file mode 100644 index 0000000..ec19bba --- /dev/null +++ b/libseat/backend/logind.c @@ -0,0 +1,782 @@ +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/sysmacros.h> +#include <sys/un.h> +#include <unistd.h> + +#if defined(HAVE_ELOGIND) +#include <elogind/sd-bus.h> +#include <elogind/sd-login.h> +#elif defined(HAVE_SYSTEMD) +#include <systemd/sd-bus.h> +#include <systemd/sd-login.h> +#else +#error logind backend requires either elogind or systemd +#endif + +#include "backend.h" +#include "drm.h" +#include "libseat.h" +#include "list.h" + +struct backend_logind { + struct libseat base; + struct libseat_seat_listener *seat_listener; + void *seat_listener_data; + + sd_bus *bus; + char *id; + char *seat; + char *path; + char *seat_path; + + bool can_graphical; + bool active; + bool initial_setup; + int has_drm; +}; + +const struct libseat_impl logind_impl; +static struct backend_logind *backend_logind_from_libseat_backend(struct libseat *base); + +static void destroy(struct backend_logind *backend) { + assert(backend); + if (backend->bus != NULL) { + sd_bus_unref(backend->bus); + } + free(backend->id); + free(backend->seat); + free(backend->path); + free(backend->seat_path); + free(backend); +} + +static int close_seat(struct libseat *base) { + struct backend_logind *backend = backend_logind_from_libseat_backend(base); + destroy(backend); + return 0; +} + +static int open_device(struct libseat *base, const char *path, int *fd) { + struct backend_logind *session = backend_logind_from_libseat_backend(base); + + int ret; + int tmpfd = -1; + sd_bus_message *msg = NULL; + sd_bus_error error = SD_BUS_ERROR_NULL; + + struct stat st; + if (stat(path, &st) < 0) { + return -1; + } + + ret = sd_bus_call_method(session->bus, "org.freedesktop.login1", session->path, + "org.freedesktop.login1.Session", "TakeDevice", &error, &msg, "uu", + major(st.st_rdev), minor(st.st_rdev)); + if (ret < 0) { + tmpfd = -1; + goto out; + } + + int paused = 0; + ret = sd_bus_message_read(msg, "hb", &tmpfd, &paused); + if (ret < 0) { + tmpfd = -1; + goto out; + } + + // The original fd seems to be closed when the message is freed + // so we just clone it. + tmpfd = fcntl(tmpfd, F_DUPFD_CLOEXEC, 0); + if (tmpfd < 0) { + tmpfd = -1; + goto out; + } + + if (dev_is_drm(st.st_rdev)) { + session->has_drm++; + } + + *fd = tmpfd; +out: + sd_bus_error_free(&error); + sd_bus_message_unref(msg); + return tmpfd; +} + +static int close_device(struct libseat *base, int device_id) { + struct backend_logind *session = backend_logind_from_libseat_backend(base); + if (device_id < 0) { + return -1; + } + + int fd = device_id; + + struct stat st = {0}; + if (fstat(fd, &st) < 0) { + close(fd); + return -1; + } + if (dev_is_drm(st.st_rdev)) { + session->has_drm--; + assert(session->has_drm >= 0); + } + close(fd); + + sd_bus_message *msg = NULL; + sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus_call_method(session->bus, "org.freedesktop.login1", session->path, + "org.freedesktop.login1.Session", "ReleaseDevice", &error, &msg, "uu", + major(st.st_rdev), minor(st.st_rdev)); + + sd_bus_error_free(&error); + sd_bus_message_unref(msg); + + return -1; +} + +static int switch_session(struct libseat *base, int s) { + struct backend_logind *session = backend_logind_from_libseat_backend(base); + if (s >= UINT16_MAX || s < 0) { + return -1; + } + + // Only seat0 has VTs associated with it + if (strcmp(session->seat, "seat0") != 0) { + return true; + } + + int ret; + sd_bus_message *msg = NULL; + sd_bus_error error = SD_BUS_ERROR_NULL; + + ret = sd_bus_call_method(session->bus, "org.freedesktop.login1", + "/org/freedesktop/login1/seat/seat0", "org.freedesktop.login1.Seat", + "SwitchTo", &error, &msg, "u", (uint32_t)s); + + sd_bus_error_free(&error); + sd_bus_message_unref(msg); + return ret >= 0; +} + +static int disable_seat(struct libseat *base) { + (void)base; + return 0; +} + +static int get_fd(struct libseat *base) { + struct backend_logind *backend = backend_logind_from_libseat_backend(base); + return sd_bus_get_fd(backend->bus); +} + +static int poll_connection(struct backend_logind *backend, int timeout) { + struct pollfd fd = { + .fd = sd_bus_get_fd(backend->bus), + .events = POLLIN, + }; + + if (poll(&fd, 1, timeout) == -1) { + if (errno == EAGAIN || errno == EINTR) { + return 0; + } else { + return -1; + } + } + + if (fd.revents & (POLLERR | POLLHUP)) { + return -1; + } + return 0; +} + +static int dispatch_background(struct libseat *base, int timeout) { + struct backend_logind *backend = backend_logind_from_libseat_backend(base); + if (backend->initial_setup) { + backend->initial_setup = false; + if (backend->active) { + backend->seat_listener->enable_seat(&backend->base, + backend->seat_listener_data); + } else { + backend->seat_listener->disable_seat(&backend->base, + backend->seat_listener_data); + } + } + + int total_dispatched = 0; + int dispatched = 0; + while ((dispatched = sd_bus_process(backend->bus, NULL)) > 0) { + total_dispatched += dispatched; + } + if (total_dispatched == 0 && timeout != 0) { + if (poll_connection(backend, timeout) == -1) { + return -1; + } + while ((dispatched = sd_bus_process(backend->bus, NULL)) > 0) { + total_dispatched += dispatched; + } + } + return total_dispatched; +} + +static const char *seat_name(struct libseat *base) { + struct backend_logind *backend = backend_logind_from_libseat_backend(base); + + if (backend->seat == NULL) { + return NULL; + } + return backend->seat; +} + +static struct backend_logind *backend_logind_from_libseat_backend(struct libseat *base) { + assert(base->impl == &logind_impl); + return (struct backend_logind *)base; +} + +static bool session_activate(struct backend_logind *session) { + sd_bus_message *msg = NULL; + sd_bus_error error = SD_BUS_ERROR_NULL; + + int ret = sd_bus_call_method(session->bus, "org.freedesktop.login1", session->path, + "org.freedesktop.login1.Session", "Activate", &error, &msg, ""); + + sd_bus_error_free(&error); + sd_bus_message_unref(msg); + return ret >= 0; +} + +static bool take_control(struct backend_logind *session) { + sd_bus_message *msg = NULL; + sd_bus_error error = SD_BUS_ERROR_NULL; + + int ret = sd_bus_call_method(session->bus, "org.freedesktop.login1", session->path, + "org.freedesktop.login1.Session", "TakeControl", &error, &msg, + "b", false); + + sd_bus_error_free(&error); + sd_bus_message_unref(msg); + return ret >= 0; +} + +static void set_active(struct backend_logind *backend, bool active) { + if (backend->active == active) { + return; + } + + backend->active = active; + if (active) { + backend->seat_listener->enable_seat(&backend->base, backend->seat_listener_data); + } else { + backend->seat_listener->disable_seat(&backend->base, backend->seat_listener_data); + } +} + +static int pause_device(sd_bus_message *msg, void *userdata, sd_bus_error *ret_error) { + struct backend_logind *session = userdata; + + uint32_t major, minor; + const char *type; + int ret = sd_bus_message_read(msg, "uus", &major, &minor, &type); + if (ret < 0) { + goto error; + } + + if (dev_is_drm(makedev(major, minor)) && strcmp(type, "gone") != 0) { + assert(session->has_drm > 0); + set_active(session, false); + } + + if (strcmp(type, "pause") == 0) { + sd_bus_call_method(session->bus, "org.freedesktop.login1", session->path, + "org.freedesktop.login1.Session", "PauseDeviceComplete", + ret_error, &msg, "uu", major, minor); + } + +error: + return 0; +} + +static int resume_device(sd_bus_message *msg, void *userdata, sd_bus_error *ret_error) { + (void)ret_error; + struct backend_logind *session = userdata; + int ret; + + int fd; + uint32_t major, minor; + ret = sd_bus_message_read(msg, "uuh", &major, &minor, &fd); + if (ret < 0) { + goto error; + } + + if (dev_is_drm(makedev(major, minor))) { + assert(session->has_drm > 0); + set_active(session, true); + } + +error: + return 0; +} + +static int session_properties_changed(sd_bus_message *msg, void *userdata, sd_bus_error *ret_error) { + (void)ret_error; + struct backend_logind *session = userdata; + int ret = 0; + + if (session->has_drm > 0) { + return 0; + } + + // PropertiesChanged arg 1: interface + const char *interface; + ret = sd_bus_message_read_basic(msg, 's', &interface); // skip path + if (ret < 0) { + goto error; + } + + if (strcmp(interface, "org.freedesktop.login1.Session") != 0) { + // not interesting for us; ignore + return 0; + } + + // PropertiesChanged arg 2: changed properties with values + ret = sd_bus_message_enter_container(msg, 'a', "{sv}"); + if (ret < 0) { + goto error; + } + + const char *s; + while ((ret = sd_bus_message_enter_container(msg, 'e', "sv")) > 0) { + ret = sd_bus_message_read_basic(msg, 's', &s); + if (ret < 0) { + goto error; + } + + if (strcmp(s, "Active") == 0) { + int ret; + ret = sd_bus_message_enter_container(msg, 'v', "b"); + if (ret < 0) { + goto error; + } + + bool active; + ret = sd_bus_message_read_basic(msg, 'b', &active); + if (ret < 0) { + goto error; + } + + set_active(session, active); + return 0; + } else { + sd_bus_message_skip(msg, "{sv}"); + } + + ret = sd_bus_message_exit_container(msg); + if (ret < 0) { + goto error; + } + } + + if (ret < 0) { + goto error; + } + + ret = sd_bus_message_exit_container(msg); + if (ret < 0) { + goto error; + } + + // PropertiesChanged arg 3: changed properties without values + sd_bus_message_enter_container(msg, 'a', "s"); + while ((ret = sd_bus_message_read_basic(msg, 's', &s)) > 0) { + if (strcmp(s, "Active") == 0) { + sd_bus_error error = SD_BUS_ERROR_NULL; + bool active; + ret = sd_bus_get_property_trivial(session->bus, "org.freedesktop.login1", + session->path, + "org.freedesktop.login1.Session", + "Active", &error, 'b', &active); + if (ret < 0) { + return 0; + } + + set_active(session, active); + return 0; + } + } + + if (ret < 0) { + goto error; + } + + return 0; + +error: + return 0; +} + +static int seat_properties_changed(sd_bus_message *msg, void *userdata, sd_bus_error *ret_error) { + (void)ret_error; + struct backend_logind *session = userdata; + int ret = 0; + + if (session->has_drm > 0) { + return 0; + } + + // PropertiesChanged arg 1: interface + const char *interface; + ret = sd_bus_message_read_basic(msg, 's', &interface); // skip path + if (ret < 0) { + goto error; + } + + if (strcmp(interface, "org.freedesktop.login1.Seat") != 0) { + // not interesting for us; ignore + return 0; + } + + // PropertiesChanged arg 2: changed properties with values + ret = sd_bus_message_enter_container(msg, 'a', "{sv}"); + if (ret < 0) { + goto error; + } + + const char *s; + while ((ret = sd_bus_message_enter_container(msg, 'e', "sv")) > 0) { + ret = sd_bus_message_read_basic(msg, 's', &s); + if (ret < 0) { + goto error; + } + + if (strcmp(s, "CanGraphical") == 0) { + int ret; + ret = sd_bus_message_enter_container(msg, 'v', "b"); + if (ret < 0) { + goto error; + } + + ret = sd_bus_message_read_basic(msg, 'b', &session->can_graphical); + if (ret < 0) { + goto error; + } + + return 0; + } else { + sd_bus_message_skip(msg, "{sv}"); + } + + ret = sd_bus_message_exit_container(msg); + if (ret < 0) { + goto error; + } + } + + if (ret < 0) { + goto error; + } + + ret = sd_bus_message_exit_container(msg); + if (ret < 0) { + goto error; + } + + // PropertiesChanged arg 3: changed properties without values + sd_bus_message_enter_container(msg, 'a', "s"); + while ((ret = sd_bus_message_read_basic(msg, 's', &s)) > 0) { + if (strcmp(s, "CanGraphical") == 0) { + session->can_graphical = sd_seat_can_graphical(session->seat); + return 0; + } + } + + if (ret < 0) { + goto error; + } + + return 0; + +error: + return 0; +} + +static bool add_signal_matches(struct backend_logind *backend) { + static const char *logind = "org.freedesktop.login1"; + static const char *session_interface = "org.freedesktop.login1.Session"; + static const char *property_interface = "org.freedesktop.DBus.Properties"; + int ret; + + ret = sd_bus_match_signal(backend->bus, NULL, logind, backend->path, session_interface, + "PauseDevice", pause_device, backend); + if (ret < 0) { + return false; + } + + ret = sd_bus_match_signal(backend->bus, NULL, logind, backend->path, session_interface, + "ResumeDevice", resume_device, backend); + if (ret < 0) { + return false; + } + + ret = sd_bus_match_signal(backend->bus, NULL, logind, backend->path, property_interface, + "PropertiesChanged", session_properties_changed, backend); + if (ret < 0) { + return false; + } + + ret = sd_bus_match_signal(backend->bus, NULL, logind, backend->seat_path, property_interface, + "PropertiesChanged", seat_properties_changed, backend); + if (ret < 0) { + return false; + } + + return true; +} + +static bool contains_str(const char *needle, const char **haystack) { + for (int i = 0; haystack[i]; i++) { + if (strcmp(haystack[i], needle) == 0) { + return true; + } + } + + return false; +} + +static bool get_greeter_session(char **session_id) { + char *class = NULL; + char **user_sessions = NULL; + int user_session_count = sd_uid_get_sessions(getuid(), 1, &user_sessions); + + if (user_session_count < 0) { + goto out; + } + + if (user_session_count == 0) { + goto out; + } + + for (int i = 0; i < user_session_count; ++i) { + int ret = sd_session_get_class(user_sessions[i], &class); + if (ret < 0) { + continue; + } + + if (strcmp(class, "greeter") == 0) { + *session_id = strdup(user_sessions[i]); + goto out; + } + } + +out: + free(class); + for (int i = 0; i < user_session_count; ++i) { + free(user_sessions[i]); + } + free(user_sessions); + + return *session_id != NULL; +} + +static bool find_session_path(struct backend_logind *session) { + int ret; + sd_bus_message *msg = NULL; + sd_bus_error error = SD_BUS_ERROR_NULL; + + ret = sd_bus_call_method(session->bus, "org.freedesktop.login1", "/org/freedesktop/login1", + "org.freedesktop.login1.Manager", "GetSession", &error, &msg, "s", + session->id); + if (ret < 0) { + goto out; + } + + const char *path; + ret = sd_bus_message_read(msg, "o", &path); + if (ret < 0) { + goto out; + } + session->path = strdup(path); + +out: + sd_bus_error_free(&error); + sd_bus_message_unref(msg); + + return ret >= 0; +} + +static bool find_seat_path(struct backend_logind *session) { + int ret; + sd_bus_message *msg = NULL; + sd_bus_error error = SD_BUS_ERROR_NULL; + + ret = sd_bus_call_method(session->bus, "org.freedesktop.login1", "/org/freedesktop/login1", + "org.freedesktop.login1.Manager", "GetSeat", &error, &msg, "s", + session->seat); + if (ret < 0) { + goto out; + } + + const char *path; + ret = sd_bus_message_read(msg, "o", &path); + if (ret < 0) { + goto out; + } + session->seat_path = strdup(path); + +out: + sd_bus_error_free(&error); + sd_bus_message_unref(msg); + + return ret >= 0; +} + +static bool get_display_session(char **session_id) { + assert(session_id != NULL); + char *xdg_session_id = getenv("XDG_SESSION_ID"); + char *type = NULL; + char *state = NULL; + + if (xdg_session_id) { + // This just checks whether the supplied session ID is valid + if (sd_session_is_active(xdg_session_id) < 0) { + goto error; + } + *session_id = strdup(xdg_session_id); + return true; + } + + // If there's a session active for the current process then just use + // that + int ret = sd_pid_get_session(getpid(), session_id); + if (ret == 0) { + return true; + } + + // Find any active sessions for the user if the process isn't part of an + // active session itself + ret = sd_uid_get_display(getuid(), session_id); + if (ret < 0 && ret != -ENODATA) { + goto error; + } + + if (ret != 0 && !get_greeter_session(session_id)) { + goto error; + } + + assert(*session_id != NULL); + + // Check that the available session is graphical + ret = sd_session_get_type(*session_id, &type); + if (ret < 0) { + goto error; + } + + const char *graphical_session_types[] = {"wayland", "x11", "mir", NULL}; + if (!contains_str(type, graphical_session_types)) { + goto error; + } + + // Check that the session is active + ret = sd_session_get_state(*session_id, &state); + if (ret < 0) { + goto error; + } + + const char *active_states[] = {"active", "online", NULL}; + if (!contains_str(state, active_states)) { + goto error; + } + + free(type); + free(state); + return true; + +error: + free(type); + free(state); + free(*session_id); + *session_id = NULL; + + return false; +} + +static struct libseat *logind_open_seat(struct libseat_seat_listener *listener, void *data) { + struct backend_logind *backend = calloc(1, sizeof(struct backend_logind)); + if (backend == NULL) { + return NULL; + } + + if (!get_display_session(&backend->id)) { + goto error; + } + + int ret = sd_session_get_seat(backend->id, &backend->seat); + if (ret < 0) { + goto error; + } + + ret = sd_bus_default_system(&backend->bus); + if (ret < 0) { + goto error; + } + + if (!find_session_path(backend)) { + goto error; + } + + if (!find_seat_path(backend)) { + goto error; + } + + if (!add_signal_matches(backend)) { + goto error; + } + + if (!session_activate(backend)) { + goto error; + } + + if (!take_control(backend)) { + goto error; + } + + backend->can_graphical = sd_seat_can_graphical(backend->seat); + while (!backend->can_graphical) { + if (poll_connection(backend, -1) == -1) { + goto error; + } + } + + backend->initial_setup = true; + backend->active = true; + backend->seat_listener = listener; + backend->seat_listener_data = data; + backend->base.impl = &logind_impl; + + return &backend->base; + +error: + if (backend != NULL) { + destroy(backend); + } + return NULL; +} + +const struct libseat_impl logind_impl = { + .open_seat = logind_open_seat, + .disable_seat = disable_seat, + .close_seat = close_seat, + .seat_name = seat_name, + .open_device = open_device, + .close_device = close_device, + .switch_session = switch_session, + .get_fd = get_fd, + .dispatch = dispatch_background, +}; diff --git a/libseat/backend/seatd.c b/libseat/backend/seatd.c new file mode 100644 index 0000000..3b4a04b --- /dev/null +++ b/libseat/backend/seatd.c @@ -0,0 +1,545 @@ +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <unistd.h> + +#include "backend.h" +#include "connection.h" +#include "libseat.h" +#include "list.h" +#include "log.h" +#include "protocol.h" + +#ifdef BUILTIN_ENABLED +#include "poller.h" +#include "server.h" +#endif + +const struct libseat_impl seatd_impl; +const struct libseat_impl builtin_impl; + +struct pending_event { + int opcode; +}; + +struct backend_seatd { + struct libseat base; + struct connection connection; + struct libseat_seat_listener *seat_listener; + void *seat_listener_data; + struct list pending_events; + + char seat_name[MAX_SEAT_LEN]; +}; + +static int set_nonblock(int fd) { + int flags; + if ((flags = fcntl(fd, F_GETFD)) == -1 || fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) { + return -1; + } + if ((flags = fcntl(fd, F_GETFL)) == -1 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { + return -1; + } + return 0; +} + +static int seatd_connect(void) { + union { + struct sockaddr_un unix; + struct sockaddr generic; + } addr = {0}; + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd == -1) { + return -1; + } + if (set_nonblock(fd) == -1) { + close(fd); + return -1; + } + char *path = getenv("SEATD_SOCK"); + if (path == NULL) { + path = "/run/seatd.sock"; + } + addr.unix.sun_family = AF_UNIX; + strncpy(addr.unix.sun_path, path, sizeof addr.unix.sun_path); + socklen_t size = offsetof(struct sockaddr_un, sun_path) + strlen(addr.unix.sun_path); + if (connect(fd, &addr.generic, size) == -1) { + close(fd); + return -1; + }; + return fd; +} + +static struct backend_seatd *backend_seatd_from_libseat_backend(struct libseat *base) { + assert(base); +#ifdef BUILTIN_ENABLED + assert(base->impl == &seatd_impl || base->impl == &builtin_impl); +#else + assert(base->impl == &seatd_impl); +#endif + return (struct backend_seatd *)base; +} + +static void handle_enable_seat(struct backend_seatd *backend) { + if (backend->seat_listener != NULL && backend->seat_listener->enable_seat != NULL) { + backend->seat_listener->enable_seat(&backend->base, backend->seat_listener_data); + } +} + +static void handle_disable_seat(struct backend_seatd *backend) { + if (backend->seat_listener != NULL && backend->seat_listener->disable_seat != NULL) { + backend->seat_listener->disable_seat(&backend->base, backend->seat_listener_data); + } +} + +static size_t read_header(struct connection *connection, uint16_t expected_opcode) { + struct proto_header header; + if (connection_get(connection, &header, sizeof header) == -1) { + return SIZE_MAX; + } + if (header.opcode != expected_opcode) { + connection_restore(connection, sizeof header); + errno = EBADMSG; + return SIZE_MAX; + } + + return header.size; +} + +static int queue_event(struct backend_seatd *backend, int opcode) { + struct pending_event *ev = calloc(1, sizeof(struct pending_event)); + if (ev == NULL) { + return -1; + } + + ev->opcode = opcode; + list_add(&backend->pending_events, ev); + return 0; +} + +static void execute_events(struct backend_seatd *backend) { + while (backend->pending_events.length > 0) { + struct pending_event *ev = list_pop_front(&backend->pending_events); + int opcode = ev->opcode; + free(ev); + + switch (opcode) { + case SERVER_DISABLE_SEAT: + handle_disable_seat(backend); + break; + case SERVER_ENABLE_SEAT: + handle_enable_seat(backend); + break; + default: + abort(); + } + } +} + +static int dispatch_pending(struct backend_seatd *backend, int *opcode) { + int packets = 0; + struct proto_header header; + while (connection_get(&backend->connection, &header, sizeof header) != -1) { + packets++; + switch (header.opcode) { + case SERVER_DISABLE_SEAT: + case SERVER_ENABLE_SEAT: + queue_event(backend, header.opcode); + break; + default: + if (opcode != NULL && + connection_pending(&backend->connection) >= header.size) { + *opcode = header.opcode; + } + connection_restore(&backend->connection, sizeof header); + return packets; + } + } + return packets; +} + +static int poll_connection(struct backend_seatd *backend, int timeout) { + struct pollfd fd = { + .fd = backend->connection.fd, + .events = POLLIN, + }; + + if (poll(&fd, 1, timeout) == -1) { + return (errno == EAGAIN || errno == EINTR) ? 0 : -1; + } + + if (fd.revents & (POLLERR | POLLHUP)) { + errno = EPIPE; + return -1; + } + + int len = 0; + if (fd.revents & POLLIN) { + len = connection_read(&backend->connection); + if (len == 0 || (len == -1 && errno != EAGAIN)) { + return -1; + } + } + + return len; +} + +static int dispatch(struct backend_seatd *backend) { + if (connection_flush(&backend->connection) == -1) { + return -1; + } + int opcode = 0; + while (dispatch_pending(backend, &opcode) == 0 && opcode == 0) { + if (poll_connection(backend, -1) == -1) { + return -1; + } + } + return 0; +} + +static void check_error(struct connection *connection) { + struct proto_header header; + if (connection_get(connection, &header, sizeof header) == -1) { + return; + } + if (header.opcode != SERVER_ERROR) { + errno = EBADMSG; + return; + } + + struct proto_server_error msg; + if (connection_get(connection, &msg, sizeof msg) == -1) { + return; + } + + errno = msg.error_code; +} + +static int get_fd(struct libseat *base) { + struct backend_seatd *backend = backend_seatd_from_libseat_backend(base); + return backend->connection.fd; +} + +static int dispatch_background(struct libseat *base, int timeout) { + struct backend_seatd *backend = backend_seatd_from_libseat_backend(base); + int dispatched = dispatch_pending(backend, NULL); + if (dispatched > 0) { + // We don't want to block if we dispatched something, as the + // caller might be waiting for the result. However, we'd also + // like to read anything pending. + timeout = 0; + } + int read = 0; + if (timeout == 0) { + read = connection_read(&backend->connection); + } else { + read = poll_connection(backend, timeout); + } + if (read > 0) { + dispatched += dispatch_pending(backend, NULL); + } else if (read == -1 && errno != EAGAIN) { + return -1; + } + + execute_events(backend); + return dispatched; +} + +static void destroy(struct backend_seatd *backend) { + if (backend->connection.fd != -1) { + close(backend->connection.fd); + backend->connection.fd = -1; + } + connection_close_fds(&backend->connection); + for (size_t idx = 0; idx < backend->pending_events.length; idx++) { + free(backend->pending_events.items[idx]); + } + list_free(&backend->pending_events); + free(backend); +} + +static struct libseat *_open_seat(struct libseat_seat_listener *listener, void *data, int fd) { + struct backend_seatd *backend = calloc(1, sizeof(struct backend_seatd)); + if (backend == NULL) { + close(fd); + return NULL; + } + backend->seat_listener = listener; + backend->seat_listener_data = data; + backend->connection.fd = fd; + backend->base.impl = &seatd_impl; + list_init(&backend->pending_events); + + struct proto_header header = { + .opcode = CLIENT_OPEN_SEAT, + .size = 0, + }; + + if (connection_put(&backend->connection, &header, sizeof header) == -1 || + dispatch(backend) == -1) { + destroy(backend); + return NULL; + } + + size_t size = read_header(&backend->connection, SERVER_SEAT_OPENED); + if (size == SIZE_MAX) { + check_error(&backend->connection); + destroy(backend); + return NULL; + } + + struct proto_server_seat_opened rmsg; + if (sizeof rmsg > size) { + errno = EBADMSG; + return NULL; + } + + if (connection_get(&backend->connection, &rmsg, sizeof rmsg) == -1) { + return NULL; + }; + + if (sizeof rmsg + rmsg.seat_name_len > size || + rmsg.seat_name_len >= sizeof backend->seat_name) { + errno = EBADMSG; + return NULL; + } + + if (connection_get(&backend->connection, backend->seat_name, rmsg.seat_name_len) == -1) { + return NULL; + }; + + return &backend->base; +} + +static struct libseat *open_seat(struct libseat_seat_listener *listener, void *data) { + int fd = seatd_connect(); + if (fd == -1) { + return NULL; + } + + return _open_seat(listener, data, fd); +} + +static int close_seat(struct libseat *base) { + struct backend_seatd *backend = backend_seatd_from_libseat_backend(base); + + struct proto_header header = { + .opcode = CLIENT_CLOSE_SEAT, + .size = 0, + }; + + if (connection_put(&backend->connection, &header, sizeof header) == -1 || + dispatch(backend) == -1) { + destroy(backend); + return -1; + } + + size_t size = read_header(&backend->connection, SERVER_SEAT_CLOSED); + if (size == SIZE_MAX) { + check_error(&backend->connection); + destroy(backend); + return -1; + } + + destroy(backend); + return 0; +} + +static const char *seat_name(struct libseat *base) { + struct backend_seatd *backend = backend_seatd_from_libseat_backend(base); + return backend->seat_name; +} + +static int open_device(struct libseat *base, const char *path, int *fd) { + struct backend_seatd *backend = backend_seatd_from_libseat_backend(base); + + size_t pathlen = strlen(path) + 1; + if (pathlen > MAX_PATH_LEN) { + errno = EINVAL; + return -1; + } + + struct proto_client_open_device msg = { + .path_len = (uint16_t)pathlen, + }; + struct proto_header header = { + .opcode = CLIENT_OPEN_DEVICE, + .size = sizeof msg + pathlen, + }; + + if (connection_put(&backend->connection, &header, sizeof header) == -1 || + connection_put(&backend->connection, &msg, sizeof msg) == -1 || + connection_put(&backend->connection, path, pathlen) == -1 || dispatch(backend) == -1) { + return -1; + } + + size_t size = read_header(&backend->connection, SERVER_DEVICE_OPENED); + if (size == SIZE_MAX) { + check_error(&backend->connection); + return -1; + } + + struct proto_server_device_opened rmsg; + if (sizeof rmsg > size) { + errno = EBADMSG; + return -1; + } + if (connection_get(&backend->connection, &rmsg, sizeof rmsg) == -1) { + return -1; + } + + int received_fd = connection_get_fd(&backend->connection); + if (received_fd == -1) { + return -1; + } + + *fd = received_fd; + return rmsg.device_id; +} + +static int close_device(struct libseat *base, int device_id) { + struct backend_seatd *backend = backend_seatd_from_libseat_backend(base); + if (device_id < 0) { + errno = EINVAL; + return -1; + } + + struct proto_client_close_device msg = { + .device_id = device_id, + }; + struct proto_header header = { + .opcode = CLIENT_CLOSE_DEVICE, + .size = sizeof msg, + }; + + if (connection_put(&backend->connection, &header, sizeof header) == -1 || + connection_put(&backend->connection, &msg, sizeof msg) == -1 || dispatch(backend) == -1) { + return -1; + } + + size_t size = read_header(&backend->connection, SERVER_DEVICE_CLOSED); + if (size == SIZE_MAX) { + check_error(&backend->connection); + return -1; + } + + struct proto_server_device_closed rmsg; + if (sizeof rmsg > size) { + errno = EBADMSG; + return -1; + } + if (connection_get(&backend->connection, &rmsg, sizeof rmsg) == -1) { + return -1; + } + if (rmsg.device_id != device_id) { + errno = EBADMSG; + return -1; + } + + return 0; +} + +static int switch_session(struct libseat *base, int session) { + struct backend_seatd *backend = backend_seatd_from_libseat_backend(base); + if (session < 0) { + return -1; + } + + struct proto_client_switch_session msg = { + .session = session, + }; + struct proto_header header = { + .opcode = CLIENT_SWITCH_SESSION, + .size = sizeof msg, + }; + + if (connection_put(&backend->connection, &header, sizeof header) == -1 || + connection_put(&backend->connection, &msg, sizeof msg) == -1 || + connection_flush(&backend->connection) == -1) { + return -1; + } + + return 0; +} + +static int disable_seat(struct libseat *base) { + struct backend_seatd *backend = backend_seatd_from_libseat_backend(base); + struct proto_header header = { + .opcode = CLIENT_DISABLE_SEAT, + .size = 0, + }; + + if (connection_put(&backend->connection, &header, sizeof header) == -1 || + connection_flush(&backend->connection) == -1) { + return -1; + } + + return 0; +} + +const struct libseat_impl seatd_impl = { + .open_seat = open_seat, + .disable_seat = disable_seat, + .close_seat = close_seat, + .seat_name = seat_name, + .open_device = open_device, + .close_device = close_device, + .switch_session = switch_session, + .get_fd = get_fd, + .dispatch = dispatch_background, +}; + +#ifdef BUILTIN_ENABLED +static struct libseat *builtin_open_seat(struct libseat_seat_listener *listener, void *data) { + int fds[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == -1) { + return NULL; + } + + pid_t pid = fork(); + if (pid == -1) { + close(fds[0]); + close(fds[1]); + return NULL; + } else if (pid == 0) { + int fd = fds[0]; + struct server *server = server_create(); + if (server == NULL) { + close(fd); + exit(1); + } + if (server_add_client(server, fd) == -1) { + exit(1); + } + while (server->running) { + if (poller_poll(server->poller) == -1) { + exit(1); + } + } + close(fd); + exit(0); + } else { + int fd = fds[1]; + return _open_seat(listener, data, fd); + } +} + +const struct libseat_impl builtin_impl = { + .open_seat = builtin_open_seat, + .disable_seat = disable_seat, + .close_seat = close_seat, + .seat_name = seat_name, + .open_device = open_device, + .close_device = close_device, + .switch_session = switch_session, + .get_fd = get_fd, + .dispatch = dispatch_background, +}; +#endif diff --git a/libseat/libseat.c b/libseat/libseat.c new file mode 100644 index 0000000..a1aed18 --- /dev/null +++ b/libseat/libseat.c @@ -0,0 +1,111 @@ +#include <assert.h> +#include <errno.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "backend.h" +#include "compiler.h" +#include "libseat.h" +#include "log.h" + +extern const struct libseat_impl seatd_impl; +extern const struct libseat_impl logind_impl; +extern const struct libseat_impl builtin_impl; + +static const struct named_backend impls[] = { +#ifdef SEATD_ENABLED + {"seatd", &seatd_impl}, +#endif +#ifdef LOGIND_ENABLED + {"logind", &logind_impl}, +#endif +#ifdef BUILTIN_ENABLED + {"builtin", &builtin_impl}, +#endif + {NULL, NULL}, +}; + +#if !defined(SEATD_ENABLED) && !defined(LOGIND_ENABLED) && !defined(BUILTIN_ENABLED) +#error At least one backend must be enabled +#endif + +LIBSEAT_EXPORT struct libseat *libseat_open_seat(struct libseat_seat_listener *listener, void *data) { + if (listener == NULL) { + errno = EINVAL; + return NULL; + } + + char *loglevel = getenv("SEATD_LOGLEVEL"); + enum libseat_log_level level = LIBSEAT_SILENT; + if (loglevel != NULL) { + if (strcmp(loglevel, "silent") == 0) { + level = LIBSEAT_SILENT; + } else if (strcmp(loglevel, "info") == 0) { + level = LIBSEAT_INFO; + } else if (strcmp(loglevel, "debug") == 0) { + level = LIBSEAT_DEBUG; + } + } + libseat_log_init(level); + + char *backend_type = getenv("LIBSEAT_BACKEND"); + struct libseat *backend = NULL; + for (const struct named_backend *iter = impls; iter->backend != NULL; iter++) { + log_debugf("libseat_open_seat: trying backend '%s'", iter->name); + if (backend_type != NULL && strcmp(backend_type, iter->name) != 0) { + continue; + } + backend = iter->backend->open_seat(listener, data); + if (backend != NULL) { + log_infof("libseat_open_seat: seat opened with backend '%s'", iter->name); + break; + } + } + if (backend == NULL) { + errno = ENOSYS; + } + return backend; +} + +LIBSEAT_EXPORT int libseat_disable_seat(struct libseat *seat) { + assert(seat && seat->impl); + return seat->impl->disable_seat(seat); +} + +LIBSEAT_EXPORT int libseat_close_seat(struct libseat *seat) { + assert(seat && seat->impl); + return seat->impl->close_seat(seat); +} + +LIBSEAT_EXPORT const char *libseat_seat_name(struct libseat *seat) { + assert(seat && seat->impl); + return seat->impl->seat_name(seat); +} + +LIBSEAT_EXPORT int libseat_open_device(struct libseat *seat, const char *path, int *fd) { + assert(seat && seat->impl); + return seat->impl->open_device(seat, path, fd); +} + +LIBSEAT_EXPORT int libseat_close_device(struct libseat *seat, int device_id) { + assert(seat && seat->impl); + return seat->impl->close_device(seat, device_id); +} + +LIBSEAT_EXPORT int libseat_get_fd(struct libseat *seat) { + assert(seat && seat->impl); + return seat->impl->get_fd(seat); +} + +LIBSEAT_EXPORT int libseat_dispatch(struct libseat *seat, int timeout) { + assert(seat && seat->impl); + return seat->impl->dispatch(seat, timeout); +} + +LIBSEAT_EXPORT int libseat_switch_session(struct libseat *seat, int session) { + assert(seat && seat->impl); + return seat->impl->switch_session(seat, session); +} diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..9f7c9a8 --- /dev/null +++ b/meson.build @@ -0,0 +1,157 @@ +project( + 'seatd', + 'c', + version: '0.1', + meson_version: '>=0.47.0', + default_options: [ + 'c_std=c11', + 'warning_level=3', + 'werror=true', + ], +) + +add_project_arguments( + [ + '-Wundef', + '-Wunused', + '-Wlogical-op', + '-Wmissing-include-dirs', + '-Wold-style-definition', # nop + '-Wpointer-arith', + '-Wstrict-prototypes', + '-Wimplicit-fallthrough', + '-Wmissing-prototypes', + '-Wno-unknown-warning', + '-Wvla', + '-D_XOPEN_SOURCE=9000', + '-Wl,--exclude-libs=ALL', + '-fvisibility=hidden', + ], + language: 'c', +) + +# Hacks +source_root = meson.current_source_dir().split('/') +build_root = meson.build_root().split('/') +relative_dir_parts = [] +i = 0 +in_prefix = true +foreach p : build_root + if i >= source_root.length() or not in_prefix or p != source_root[i] + in_prefix = false + relative_dir_parts += '..' + endif + i += 1 +endforeach +i = 0 +in_prefix = true +foreach p : source_root + if i >= build_root.length() or not in_prefix or build_root[i] != p + in_prefix = false + relative_dir_parts += p + endif + i += 1 +endforeach + +if get_option('buildtype').startswith('debug') + add_project_arguments('-DDEBUG', language : 'c') +endif + + +add_project_arguments( + '-DLIBSEAT_REL_SRC_DIR="@0@"'.format(join_paths(relative_dir_parts) + '/'), + language: 'c', +) + +private_files = [ + 'common/connection.c', + 'common/list.c', + 'common/log.c', +] + +private_deps = [] + +server_files = [ + 'common/log.c', + 'common/list.c', + 'common/terminal.c', + 'common/connection.c', + 'common/evdev.c', + 'common/drm.c', + 'seatd/poll/basic_poller.c', + 'seatd/poll/poller.c', + 'seatd/seat.c', + 'seatd/client.c', + 'seatd/server.c', +] + +if get_option('seatd').enabled() + private_files += 'libseat/backend/seatd.c' + add_project_arguments('-DSEATD_ENABLED=1', language: 'c') +endif + +if get_option('logind').enabled() + logind = dependency('libsystemd', required: false) + add_project_arguments('-DLOGIND_ENABLED=1', language: 'c') + if logind.found() + add_project_arguments('-DHAVE_SYSTEMD=1', language: 'c') + else + logind = dependency('libelogind') + add_project_arguments('-DHAVE_ELOGIND=1', language: 'c') + endif + + private_files += [ + 'libseat/backend/logind.c', + 'common/drm.c', + ] + private_deps += logind +endif + +if get_option('builtin').enabled() + add_project_arguments('-DBUILTIN_ENABLED=1', language: 'c') + private_files += server_files +endif + +private_lib = static_library( + 'seat-private', + private_files, + dependencies: private_deps, + include_directories: [include_directories('.', 'include')], +) + +lib = library( + 'seat', # This results in the library being called 'libseat' + [ 'libseat/libseat.c' ], + link_with: private_lib, + include_directories: [include_directories('.', 'include')], + install: true, +) + +install_headers('include/libseat.h') + +pkgconfig = import('pkgconfig') +pkgconfig.generate(lib, + version: meson.project_version(), + filebase: 'libseat', + name: 'libseat', + description: 'Seat management library', +) + +if get_option('server').enabled() + executable( + 'seatd', + [ server_files, 'seatd/seatd.c' ], + include_directories: [include_directories('.', 'include')], + install: true, + ) +endif + +if get_option('examples').enabled() + executable( + 'simpletest', + ['examples/simpletest/main.c'], + link_with: [lib], + include_directories: [include_directories('.', 'include')], + install: false, + ) +endif diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..fc05dc2 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,5 @@ +option('logind', type: 'feature', value: 'disabled', description: 'logind support') +option('seatd', type: 'feature', value: 'enabled', description: 'seatd support') +option('builtin', type: 'feature', value: 'disabled', description: 'builtin seatd server') +option('server', type: 'feature', value: 'enabled', description: 'seatd server') +option('examples', type: 'feature', value: 'enabled', description: 'libseat example programs') diff --git a/seatd/client.c b/seatd/client.c new file mode 100644 index 0000000..72e652c --- /dev/null +++ b/seatd/client.c @@ -0,0 +1,484 @@ +#define _GNU_SOURCE +#include <assert.h> +#include <errno.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <unistd.h> + +#include "client.h" +#include "log.h" +#include "poller.h" +#include "protocol.h" +#include "seat.h" +#include "server.h" +#include "terminal.h" + +static int get_peer(int fd, pid_t *pid, uid_t *uid, gid_t *gid) { + struct ucred cred; + socklen_t len = sizeof cred; + if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) { + return -1; + } + *pid = cred.pid; + *uid = cred.uid; + *gid = cred.gid; + return 0; +} + +struct client *client_create(struct server *server, int client_fd) { + uid_t uid; + gid_t gid; + pid_t pid; + + if (get_peer(client_fd, &pid, &uid, &gid) == -1) { + return NULL; + } + + struct client *client = calloc(1, sizeof(struct client)); + if (client == NULL) { + return NULL; + } + + client->uid = uid; + client->gid = gid; + client->pid = pid; + client->server = server; + client->connection.fd = client_fd; + list_init(&client->devices); + return client; +} + +void client_kill(struct client *client) { + assert(client); + if (client->connection.fd != -1) { + shutdown(client->connection.fd, SHUT_RDWR); + close(client->connection.fd); + client->connection.fd = -1; + }; + if (client->seat != NULL) { + seat_remove_client(client->seat, client); + client->seat = NULL; + } +} + +void client_destroy(struct client *client) { + assert(client); + client->server = NULL; + if (client->seat != NULL) { + // This should also close and remove all devices + seat_remove_client(client->seat, client); + client->seat = NULL; + } + if (client->event_source != NULL) { + event_source_fd_destroy(client->event_source); + client->event_source = NULL; + } + if (client->connection.fd != -1) { + shutdown(client->connection.fd, SHUT_RDWR); + close(client->connection.fd); + client->connection.fd = -1; + } + connection_close_fds(&client->connection); + assert(client->devices.length == 0); + list_free(&client->devices); + free(client); +} + +static int client_flush(struct client *client) { + int ret = connection_flush(&client->connection); + if (ret == -1 && errno == EAGAIN) { + event_source_fd_update(client->event_source, EVENT_READABLE | EVENT_WRITABLE); + } else if (ret == -1) { + return -1; + } + return 0; +} + +static int client_send_error(struct client *client, int error_code) { + struct proto_server_error errmsg = { + .error_code = error_code, + }; + struct proto_header errheader = { + .opcode = SERVER_ERROR, + .size = sizeof errmsg, + }; + + if (connection_put(&client->connection, &errheader, sizeof errheader) == -1 || + connection_put(&client->connection, &errmsg, sizeof errmsg)) { + log_error("could not send error to client"); + return -1; + } + return 0; +} + +static char *client_get_seat_name(struct client *client) { + (void)client; + // TODO: Look up seat for session. + return "seat0"; +} + +static int handle_open_seat(struct client *client) { + char *seat_name = client_get_seat_name(client); + if (seat_name == NULL) { + log_error("could not get name of target seat"); + return -1; + } + + struct seat *seat = server_get_seat(client->server, seat_name); + if (seat == NULL) { + log_error("unable to find seat by name"); + return -1; + } + + if (seat_add_client(seat, client) == -1) { + log_errorf("unable to add client to target seat: %s", strerror(errno)); + return -1; + } + + size_t seat_name_len = strlen(seat_name); + + struct proto_server_seat_opened rmsg = { + .seat_name_len = (uint16_t)seat_name_len, + }; + struct proto_header header = { + .opcode = SERVER_SEAT_OPENED, + .size = sizeof rmsg + seat_name_len, + }; + + if (connection_put(&client->connection, &header, sizeof header) == -1 || + connection_put(&client->connection, &rmsg, sizeof rmsg) == -1 || + connection_put(&client->connection, seat_name, seat_name_len) == -1) { + log_errorf("unable to write response: %s", strerror(errno)); + return -1; + } + + seat_open_client(seat, client); + return 0; +} + +static int handle_close_seat(struct client *client) { + if (client->seat == NULL) { + log_error("protocol error: no seat associated with client"); + return -1; + } + + if (seat_remove_client(client->seat, client) == -1) { + log_error("unable to remove client from seat"); + return -1; + } + + struct proto_header header = { + .opcode = SERVER_SEAT_CLOSED, + .size = 0, + }; + + if (connection_put(&client->connection, &header, sizeof header) == -1) { + log_errorf("unable to write response: %s", strerror(errno)); + return -1; + } + + return 0; +} + +static int handle_open_device(struct client *client, char *path) { + if (client->seat == NULL) { + log_error("protocol error: no seat associated with client"); + return -1; + } + + struct seat_device *device = seat_open_device(client, path); + if (device == NULL) { + log_errorf("could not open device: %s", strerror(errno)); + goto fail; + } + + int dupfd = dup(device->fd); + if (dupfd == -1) { + log_errorf("could not dup fd: %s", strerror(errno)); + seat_close_device(client, device); + goto fail; + } + + if (connection_put_fd(&client->connection, dupfd) == -1) { + log_errorf("unable to queue fd for sending: %s", strerror(errno)); + return -1; + } + + struct proto_server_device_opened msg = { + .device_id = device->device_id, + }; + struct proto_header header = { + .opcode = SERVER_DEVICE_OPENED, + .size = sizeof msg, + }; + + if (connection_put(&client->connection, &header, sizeof header) == -1 || + connection_put(&client->connection, &msg, sizeof msg)) { + log_errorf("unable to write response: %s", strerror(errno)); + return -1; + } + + return 0; + +fail: + return client_send_error(client, errno); +} + +static int handle_close_device(struct client *client, int device_id) { + if (client->seat == NULL) { + log_error("protocol error: no seat associated with client"); + return -1; + } + + struct seat_device *device = seat_find_device(client, device_id); + if (device == NULL) { + log_error("no such device"); + errno = EBADF; + goto fail; + } + + if (seat_close_device(client, device) == -1) { + log_errorf("could not close device: %s", strerror(errno)); + goto fail; + } + + struct proto_server_device_closed msg = { + .device_id = device_id, + }; + struct proto_header header = { + .opcode = SERVER_DEVICE_CLOSED, + .size = sizeof msg, + }; + + if (connection_put(&client->connection, &header, sizeof header) == -1 || + connection_put(&client->connection, &msg, sizeof msg)) { + log_errorf("unable to write response: %s", strerror(errno)); + return -1; + } + + return 0; + +fail: + return client_send_error(client, errno); +} + +static int handle_switch_session(struct client *client, int session) { + if (client->seat == NULL) { + log_error("protocol error: no seat associated with client"); + return -1; + } + + struct seat *seat = client->seat; + if (seat->active_client != client) { + log_info("refusing to switch session: client requesting switch is not active"); + errno = EPERM; + goto error; + } + if (session <= 0) { + log_errorf("invalid session: %d", session); + errno = EINVAL; + goto error; + } + + if (client_get_session(client) == session) { + return 0; + } + + if (seat_set_next_session(seat, session) == -1) { + log_infof("could not queue session switch: %s", strerror(errno)); + goto error; + } + + return 0; + +error: + return client_send_error(client, errno); +} + +static int handle_disable_seat(struct client *client) { + if (client->seat == NULL) { + log_error("protocol error: no seat associated with client"); + return -1; + } + + struct seat *seat = client->seat; + if (seat->active_client != client) { + log_info("client is not currently active"); + errno = EPERM; + goto error; + } + + if (seat_close_client(seat, client) == -1) { + goto error; + } + + return 0; + +error: + return client_send_error(client, errno); +} + +static int client_handle_opcode(struct client *client, uint16_t opcode, size_t size) { + int res = 0; + switch (opcode) { + case CLIENT_OPEN_SEAT: { + if (size != 0) { + log_error("protocol error: invalid open_seat message"); + return -1; + } + res = handle_open_seat(client); + break; + } + case CLIENT_CLOSE_SEAT: { + if (size != 0) { + log_error("protocol error: invalid close_seat message"); + return -1; + } + res = handle_close_seat(client); + break; + } + case CLIENT_OPEN_DEVICE: { + char path[MAX_PATH_LEN]; + struct proto_client_open_device msg; + if (sizeof msg > size || connection_get(&client->connection, &msg, sizeof msg) == -1 || + sizeof msg + msg.path_len > size || msg.path_len > MAX_PATH_LEN) { + log_error("protocol error: invalid open_device message"); + return -1; + } + if (connection_get(&client->connection, path, msg.path_len) == -1) { + log_error("protocol error: invalid open_device message"); + return -1; + } + + res = handle_open_device(client, path); + break; + } + case CLIENT_CLOSE_DEVICE: { + struct proto_client_close_device msg; + if (sizeof msg > size || connection_get(&client->connection, &msg, sizeof msg) == -1) { + log_error("protocol error: invalid close_device message"); + return -1; + } + + res = handle_close_device(client, msg.device_id); + break; + } + case CLIENT_SWITCH_SESSION: { + struct proto_client_switch_session msg; + if (sizeof msg > size || connection_get(&client->connection, &msg, sizeof msg) == -1) { + log_error("protocol error: invalid switch_session message"); + return -1; + } + + res = handle_switch_session(client, msg.session); + break; + } + case CLIENT_DISABLE_SEAT: { + if (size != 0) { + log_error("protocol error: invalid disable_seat message"); + return -1; + } + res = handle_disable_seat(client); + break; + } + default: + log_errorf("protocol error: unknown opcode: %d", opcode); + res = -1; + break; + } + if (res != -1) { + res = client_flush(client); + } + return res; +} + +int client_disable_seat(struct client *client) { + struct proto_header header = { + .opcode = SERVER_DISABLE_SEAT, + .size = 0, + }; + if (connection_put(&client->connection, &header, sizeof header) == -1 || + connection_flush(&client->connection) == -1) { + log_error("unable to send event"); + return -1; + } + return 0; +} + +int client_enable_seat(struct client *client) { + struct proto_header header = { + .opcode = SERVER_ENABLE_SEAT, + .size = 0, + }; + if (connection_put(&client->connection, &header, sizeof header) == -1 || + connection_flush(&client->connection) == -1) { + log_error("unable to send event"); + return -1; + } + return 0; +} + +int client_handle_connection(int fd, uint32_t mask, void *data) { + (void)fd; + + struct client *client = data; + if (mask & EVENT_ERROR) { + log_error("connection error"); + goto fail; + } + if (mask & EVENT_HANGUP) { + log_info("client disconnected"); + goto fail; + } + + if (mask & EVENT_WRITABLE) { + int len = connection_flush(&client->connection); + if (len == -1 && errno != EAGAIN) { + log_error("could not flush client connection"); + goto fail; + } else if (len >= 0) { + event_source_fd_update(client->event_source, EVENT_READABLE); + } + } + + if (mask & EVENT_READABLE) { + int len = connection_read(&client->connection); + if (len == 0 || (len == -1 && errno != EAGAIN)) { + log_error("could not read client connection"); + goto fail; + } + + struct proto_header header; + while (connection_get(&client->connection, &header, sizeof header) != -1) { + if (connection_pending(&client->connection) < header.size) { + connection_restore(&client->connection, sizeof header); + break; + } + if (client_handle_opcode(client, header.opcode, header.size) == -1) { + goto fail; + } + } + } + + return 0; + +fail: + client_destroy(client); + return -1; +} + +int client_get_session(struct client *client) { + if (client->seat == NULL || client->seat->active_client != client) { + return -1; + } + if (client->seat->vt_bound) { + return client->seat->active_client->seat_vt; + } + // TODO: Store some session sequence + abort(); + return -1; +} diff --git a/seatd/poll/basic_poller.c b/seatd/poll/basic_poller.c new file mode 100644 index 0000000..a388d8e --- /dev/null +++ b/seatd/poll/basic_poller.c @@ -0,0 +1,386 @@ +#include <assert.h> +#include <errno.h> +#include <poll.h> +#include <signal.h> +#include <stddef.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +#include "list.h" +#include "poller.h" + +struct basic_poller *global_poller = NULL; + +const struct poll_impl basic_poller_impl; +const struct event_source_fd_impl basic_poller_fd_impl; +const struct event_source_signal_impl basic_poller_signal_impl; + +struct basic_poller { + struct poller base; + struct list signals; + struct list new_signals; + struct list fds; + struct list new_fds; + + struct pollfd *pollfds; + size_t pollfds_len; + bool dirty; + bool inpoll; +}; + +struct basic_poller_fd { + struct event_source_fd base; + struct basic_poller *poller; + bool killed; +}; + +struct basic_poller_signal { + struct event_source_signal base; + struct basic_poller *poller; + bool raised; + bool killed; +}; + +static struct basic_poller *basic_poller_from_poller(struct poller *base) { + assert(base->impl == &basic_poller_impl); + return (struct basic_poller *)base; +} + +static struct poller *basic_poller_create(void) { + if (global_poller != NULL) { + errno = EEXIST; + return NULL; + } + + struct basic_poller *poller = calloc(1, sizeof(struct basic_poller)); + if (poller == NULL) { + errno = ENOMEM; + return NULL; + } + list_init(&poller->fds); + list_init(&poller->new_fds); + list_init(&poller->signals); + list_init(&poller->new_signals); + poller->base.impl = &basic_poller_impl; + global_poller = poller; + return (struct poller *)poller; +} + +static int destroy(struct poller *base) { + struct basic_poller *poller = basic_poller_from_poller(base); + for (size_t idx = 0; idx < poller->fds.length; idx++) { + struct basic_poller_fd *bpfd = poller->fds.items[idx]; + free(bpfd); + } + list_free(&poller->fds); + for (size_t idx = 0; idx < poller->new_fds.length; idx++) { + struct basic_poller_fd *bpfd = poller->new_fds.items[idx]; + free(bpfd); + } + list_free(&poller->new_fds); + for (size_t idx = 0; idx < poller->signals.length; idx++) { + struct basic_poller_signal *bps = poller->signals.items[idx]; + + struct sigaction sa; + sa.sa_handler = SIG_DFL; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(bps->base.signal, &sa, NULL); + + free(bps); + } + list_free(&poller->signals); + for (size_t idx = 0; idx < poller->new_signals.length; idx++) { + struct basic_poller_signal *bps = poller->new_signals.items[idx]; + + struct sigaction sa; + sa.sa_handler = SIG_DFL; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(bps->base.signal, &sa, NULL); + + free(bps); + } + list_free(&poller->new_signals); + free(poller->pollfds); + return 0; +} + +static int event_mask_to_poll_mask(uint32_t event_mask) { + int poll_mask = 0; + if (event_mask & EVENT_READABLE) { + poll_mask |= POLLIN; + } + if (event_mask & EVENT_WRITABLE) { + poll_mask |= POLLOUT; + } + return poll_mask; +} + +static uint32_t poll_mask_to_event_mask(int poll_mask) { + uint32_t event_mask = 0; + if (poll_mask & POLLIN) { + event_mask |= EVENT_READABLE; + } + if (poll_mask & POLLOUT) { + event_mask |= EVENT_WRITABLE; + } + if (poll_mask & POLLERR) { + event_mask |= EVENT_ERROR; + } + if (poll_mask & POLLHUP) { + event_mask |= EVENT_HANGUP; + } + return event_mask; +} + +static int regenerate_pollfds(struct basic_poller *poller) { + if (poller->pollfds_len != poller->fds.length) { + struct pollfd *fds = calloc(poller->fds.length, sizeof(struct pollfd)); + if (fds == NULL) { + return -1; + } + free(poller->pollfds); + poller->pollfds = fds; + poller->pollfds_len = poller->fds.length; + } + + for (size_t idx = 0; idx < poller->fds.length; idx++) { + struct basic_poller_fd *bpfd = poller->fds.items[idx]; + poller->pollfds[idx] = (struct pollfd){ + .fd = bpfd->base.fd, + .events = event_mask_to_poll_mask(bpfd->base.mask), + }; + } + + return 0; +} + +static struct event_source_fd *add_fd(struct poller *base, int fd, uint32_t mask, + event_source_fd_func_t func, void *data) { + struct basic_poller *poller = basic_poller_from_poller(base); + + struct basic_poller_fd *bpfd = calloc(1, sizeof(struct basic_poller_fd)); + if (bpfd == NULL) { + return NULL; + } + bpfd->base.impl = &basic_poller_fd_impl; + bpfd->base.fd = fd; + bpfd->base.mask = mask; + bpfd->base.data = data; + bpfd->base.func = func; + bpfd->poller = poller; + poller->dirty = true; + if (poller->inpoll) { + list_add(&poller->new_fds, bpfd); + } else { + list_add(&poller->fds, bpfd); + regenerate_pollfds(poller); + } + return (struct event_source_fd *)bpfd; +} + +static int fd_destroy(struct event_source_fd *event_source) { + struct basic_poller_fd *bpfd = (struct basic_poller_fd *)event_source; + struct basic_poller *poller = bpfd->poller; + int idx = list_find(&poller->fds, event_source); + if (idx == -1) { + return -1; + } + poller->dirty = true; + if (poller->inpoll) { + bpfd->killed = true; + } else { + list_del(&poller->fds, idx); + free(bpfd); + regenerate_pollfds(poller); + } + return 0; +} + +static int fd_update(struct event_source_fd *event_source, uint32_t mask) { + struct basic_poller_fd *bpfd = (struct basic_poller_fd *)event_source; + struct basic_poller *poller = bpfd->poller; + event_source->mask = mask; + + poller->dirty = true; + if (!poller->inpoll) { + regenerate_pollfds(poller); + } + return 0; +} + +static void signal_handler(int sig) { + if (global_poller == NULL) { + return; + } + + for (size_t idx = 0; idx < global_poller->signals.length; idx++) { + struct basic_poller_signal *bps = global_poller->signals.items[idx]; + if (bps->base.signal == sig) { + bps->raised = true; + } + } +} + +static struct event_source_signal *add_signal(struct poller *base, int signal, + event_source_signal_func_t func, void *data) { + struct basic_poller *poller = basic_poller_from_poller(base); + + struct basic_poller_signal *bps = calloc(1, sizeof(struct basic_poller_signal)); + if (bps == NULL) { + return NULL; + } + + int refcnt = 0; + for (size_t idx = 0; idx < poller->signals.length; idx++) { + struct basic_poller_signal *bps = poller->signals.items[idx]; + if (bps->base.signal == signal) { + refcnt++; + } + } + + bps->base.impl = &basic_poller_signal_impl; + bps->base.signal = signal; + bps->base.data = data; + bps->base.func = func; + bps->poller = poller; + + if (refcnt == 0) { + struct sigaction sa; + sa.sa_handler = &signal_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(signal, &sa, NULL); + } + + if (poller->inpoll) { + list_add(&poller->new_signals, bps); + } else { + list_add(&poller->signals, bps); + } + + return (struct event_source_signal *)bps; +} + +static int signal_destroy(struct event_source_signal *event_source) { + struct basic_poller_signal *bps = (struct basic_poller_signal *)event_source; + struct basic_poller *poller = bps->poller; + + int idx = list_find(&poller->signals, event_source); + if (idx == -1) { + return -1; + } + + int refcnt = 0; + for (size_t idx = 0; idx < poller->signals.length; idx++) { + struct basic_poller_signal *b = poller->signals.items[idx]; + if (b->base.signal == bps->base.signal) { + refcnt++; + } + } + + if (refcnt == 0) { + struct sigaction sa; + sa.sa_handler = SIG_DFL; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(bps->base.signal, &sa, NULL); + } + + if (poller->inpoll) { + bps->killed = true; + } else { + list_del(&poller->signals, idx); + free(bps); + } + return 0; +} + +static int basic_poller_poll(struct poller *base) { + struct basic_poller *poller = basic_poller_from_poller(base); + + if (poll(poller->pollfds, poller->fds.length, -1) == -1 && errno != EINTR) { + return -1; + } + + poller->inpoll = true; + + for (size_t idx = 0; idx < poller->fds.length; idx++) { + short revents = poller->pollfds[idx].revents; + if (revents == 0) { + continue; + } + struct basic_poller_fd *bpfd = poller->fds.items[idx]; + bpfd->base.func(poller->pollfds[idx].fd, poll_mask_to_event_mask(revents), + bpfd->base.data); + } + + for (size_t idx = 0; idx < poller->signals.length; idx++) { + struct basic_poller_signal *bps = poller->signals.items[idx]; + if (!bps->raised) { + continue; + } + bps->base.func(bps->base.signal, bps->base.data); + bps->raised = false; + } + + poller->inpoll = false; + + for (size_t idx = 0; idx < poller->fds.length; idx++) { + struct basic_poller_fd *bpfd = poller->fds.items[idx]; + if (!bpfd->killed) { + continue; + } + + list_del(&poller->fds, idx); + free(bpfd); + idx--; + } + + for (size_t idx = 0; idx < poller->signals.length; idx++) { + struct basic_poller_signal *bps = poller->signals.items[idx]; + if (!bps->killed) { + continue; + } + + list_del(&poller->signals, idx); + free(bps); + idx--; + } + + if (poller->new_fds.length > 0) { + list_concat(&poller->fds, &poller->new_fds); + list_truncate(&poller->new_fds); + } + + if (poller->new_signals.length > 0) { + list_concat(&poller->signals, &poller->new_signals); + list_truncate(&poller->new_signals); + } + + if (poller->dirty) { + regenerate_pollfds(poller); + poller->dirty = false; + } + + return 0; +} + +const struct event_source_fd_impl basic_poller_fd_impl = { + .update = fd_update, + .destroy = fd_destroy, +}; + +const struct event_source_signal_impl basic_poller_signal_impl = { + .destroy = signal_destroy, +}; + +const struct poll_impl basic_poller_impl = { + .create = basic_poller_create, + .destroy = destroy, + .add_fd = add_fd, + .add_signal = add_signal, + .poll = basic_poller_poll, +}; diff --git a/seatd/poll/poller.c b/seatd/poll/poller.c new file mode 100644 index 0000000..db39bc0 --- /dev/null +++ b/seatd/poll/poller.c @@ -0,0 +1,53 @@ +#include "poller.h" +#include <assert.h> + +extern const struct poll_impl basic_poller_impl; + +struct poller *poller_create(void) { + // TODO: Other poll impls + return basic_poller_impl.create(); +} + +int poller_destroy(struct poller *poller) { + assert(poller); + assert(poller->impl); + return poller->impl->destroy(poller); +} + +struct event_source_fd *poller_add_fd(struct poller *poller, int fd, uint32_t mask, + event_source_fd_func_t func, void *data) { + assert(poller); + assert(poller->impl); + return poller->impl->add_fd(poller, fd, mask, func, data); +} + +int event_source_fd_destroy(struct event_source_fd *event_source) { + assert(event_source); + assert(event_source->impl); + return event_source->impl->destroy(event_source); +} + +struct event_source_signal *poller_add_signal(struct poller *poller, int signal, + event_source_signal_func_t func, void *data) { + assert(poller); + assert(poller->impl); + return poller->impl->add_signal(poller, signal, func, data); +} + +int event_source_signal_destroy(struct event_source_signal *event_source) { + assert(event_source); + assert(event_source->impl); + return event_source->impl->destroy(event_source); +} + +int event_source_fd_update(struct event_source_fd *event_source, uint32_t mask) { + assert(event_source); + assert(event_source->impl); + return event_source->impl->update(event_source, mask); +} + +int poller_poll(struct poller *poller) { + assert(poller); + assert(poller->impl); + return poller->impl->poll(poller); +} diff --git a/seatd/seat.c b/seatd/seat.c new file mode 100644 index 0000000..ab0e338 --- /dev/null +++ b/seatd/seat.c @@ -0,0 +1,521 @@ +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "client.h" +#include "drm.h" +#include "evdev.h" +#include "list.h" +#include "log.h" +#include "protocol.h" +#include "seat.h" +#include "terminal.h" + +struct seat *seat_create(const char *seat_name, bool vt_bound) { + struct seat *seat = calloc(1, sizeof(struct seat)); + if (seat == NULL) { + return NULL; + } + list_init(&seat->clients); + seat->vt_bound = vt_bound; + + seat->seat_name = strdup(seat_name); + if (seat->seat_name == NULL) { + free(seat); + return NULL; + } + + log_debugf("created seat '%s' (vt_bound: %d)", seat_name, vt_bound); + return seat; +} + +void seat_destroy(struct seat *seat) { + assert(seat); + while (seat->clients.length > 0) { + struct client *client = seat->clients.items[seat->clients.length - 1]; + // This will cause the client to remove itself from the seat + assert(client->seat); + client_kill(client); + } + + free(seat->seat_name); + free(seat); +} + +int seat_add_client(struct seat *seat, struct client *client) { + assert(seat); + assert(client); + + if (client->seat != NULL) { + log_error("cannot add client: client is already a member of a seat"); + return -1; + } + + if (seat->vt_bound && seat->active_client != NULL) { + log_error("cannot add client: seat is vt_bound and an active client already exists"); + return -1; + } + + client->seat = seat; + + list_add(&seat->clients, client); + log_debug("added client"); + return 0; +} + +int seat_remove_client(struct seat *seat, struct client *client) { + assert(seat); + assert(client); + assert(client->seat == seat); + + // We must first remove the client to avoid reactivation + bool found = false; + for (size_t idx = 0; idx < seat->clients.length; idx++) { + struct client *c = seat->clients.items[idx]; + if (client == c) { + list_del(&seat->clients, idx); + found = true; + break; + } + } + + if (!found) { + log_debug("client was not on the client list"); + } + + if (seat->next_client == client) { + seat->next_client = NULL; + } + + while (client->devices.length > 0) { + struct seat_device *device = client->devices.items[client->devices.length - 1]; + seat_close_device(client, device); + } + + if (seat->active_client == client) { + seat_close_client(seat, client); + } + + client->seat = NULL; + log_debug("removed client"); + + return found ? -1 : 0; +} + +struct seat_device *seat_find_device(struct client *client, int device_id) { + assert(client); + assert(client->seat); + assert(device_id != 0); + + for (size_t idx = 0; idx < client->devices.length; idx++) { + struct seat_device *seat_device = client->devices.items[idx]; + if (seat_device->device_id == device_id) { + return seat_device; + } + } + errno = ENOENT; + return NULL; +} + +struct seat_device *seat_open_device(struct client *client, const char *path) { + assert(client); + assert(client->seat); + assert(strlen(path) > 0); + struct seat *seat = client->seat; + + if (client != seat->active_client) { + errno = EPERM; + return NULL; + } + + char sanitized_path[MAX_PATH_LEN]; + if (realpath(path, sanitized_path) == NULL) { + log_errorf("invalid path '%s': %s", path, strerror(errno)); + return NULL; + } + + int device_id = 1; + for (size_t idx = 0; idx < client->devices.length; idx++) { + struct seat_device *device = client->devices.items[idx]; + + // If the device already exists, increase the ref count and + // return it. + if (strcmp(device->path, path) == 0) { + device->ref_cnt++; + return device; + } + + // If the device has a higher id, up our device id + if (device->device_id >= device_id) { + device_id = device->device_id + 1; + } + } + + if (client->devices.length >= MAX_SEAT_DEVICES) { + log_error("max seat devices exceeded"); + errno = EMFILE; + return NULL; + } + + const char *prefix = "/dev/"; + if (strncmp(prefix, sanitized_path, strlen(prefix)) != 0) { + log_errorf("invalid path '%s': expected device in /dev", sanitized_path); + errno = ENOENT; + return NULL; + } + + int fd = open(sanitized_path, O_RDWR | O_NOCTTY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK); + if (fd == -1) { + log_errorf("could not open file: %s", strerror(errno)); + return NULL; + } + + struct stat st; + if (fstat(fd, &st) == -1) { + log_errorf("could not fstat: %s", strerror(errno)); + close(fd); + errno = EACCES; + return NULL; + } + + if (dev_is_drm(st.st_rdev)) { + if (drm_set_master(fd) == -1) { + log_debugf("drm_set_master failed: %s", strerror(errno)); + } + } else if (dev_is_evdev(st.st_rdev)) { + // Nothing to do here + } else { + // Not a device type we want to share + log_errorf("disallowed device type for '%s': %ld", sanitized_path, st.st_rdev); + close(fd); + errno = EACCES; + return NULL; + } + + struct seat_device *device = calloc(1, sizeof(struct seat_device)); + if (device == NULL) { + log_errorf("could not alloc device for '%s': %s", sanitized_path, strerror(errno)); + close(fd); + errno = ENOMEM; + return NULL; + } + + device->path = strdup(sanitized_path); + if (device->path == NULL) { + log_errorf("could not dup path for '%s': %s", sanitized_path, strerror(errno)); + close(fd); + free(device); + return NULL; + } + + log_debugf("seat: %p, client: %p, path: '%s', device_id: %d", (void *)seat, (void *)client, + path, device_id); + + device->ref_cnt++; + device->dev = st.st_rdev; + device->fd = fd; + device->device_id = device_id; + device->active = true; + list_add(&client->devices, device); + return device; +} + +int seat_close_device(struct client *client, struct seat_device *seat_device) { + assert(client); + assert(client->seat); + assert(seat_device && seat_device->fd > 0); + + // Find the device in our list + size_t idx = list_find(&client->devices, seat_device); + if (idx == -1UL) { + log_error("seat device not registered by client"); + errno = ENOENT; + return -1; + } + + log_debugf("seat: %p, client: %p, path: '%s', device_id: %d", (void *)client->seat, + (void *)client, seat_device->path, seat_device->device_id); + + seat_device->ref_cnt--; + if (seat_device->ref_cnt > 0) { + // We still have more references to this device, so leave it be. + return 0; + } + + // The ref count hit zero, so destroy the device + list_del(&client->devices, idx); + if (seat_device->active && seat_device->fd != -1) { + if (dev_is_drm(seat_device->dev)) { + if (drm_drop_master(seat_device->fd) == -1) { + log_debugf("drm_drop_master failed: %s", strerror(errno)); + } + } else if (dev_is_evdev(seat_device->dev)) { + if (evdev_revoke(seat_device->fd) == -1) { + log_debugf("evdev_revoke failed: %s", strerror(errno)); + } + } + close(seat_device->fd); + seat_device->fd = -1; + } + free(seat_device->path); + free(seat_device); + return 0; +} + +static int seat_deactivate_device(struct client *client, struct seat_device *seat_device) { + assert(client); + assert(client->seat); + assert(seat_device && seat_device->fd > 0); + + if (!seat_device->active) { + return 0; + } + if (dev_is_drm(seat_device->dev)) { + if (drm_drop_master(seat_device->fd) == -1) { + return -1; + } + } else if (dev_is_evdev(seat_device->dev)) { + if (evdev_revoke(seat_device->fd) == -1) { + return -1; + } + } else { + errno = EACCES; + return -1; + } + seat_device->active = false; + return 0; +} + +static int seat_activate_device(struct client *client, struct seat_device *seat_device) { + assert(client); + assert(client->seat); + assert(seat_device && seat_device->fd > 0); + + if (seat_device->active) { + return 0; + } + if (dev_is_drm(seat_device->dev)) { + drm_set_master(seat_device->fd); + seat_device->active = true; + } else if (dev_is_evdev(seat_device->dev)) { + // We can't do anything here + errno = EINVAL; + return -1; + } else { + errno = EACCES; + return -1; + } + return 0; +} + +int seat_open_client(struct seat *seat, struct client *client) { + assert(seat); + assert(client); + + if (seat->vt_bound && client->seat_vt == 0) { + client->seat_vt = terminal_current_vt(); + } + + if (seat->active_client != NULL) { + log_error("client already active"); + errno = EBUSY; + return -1; + } + + if (seat->vt_bound) { + terminal_setup(client->seat_vt); + terminal_set_keyboard(client->seat_vt, false); + } + + for (size_t idx = 0; idx < client->devices.length; idx++) { + struct seat_device *device = client->devices.items[idx]; + if (seat_activate_device(client, device) == -1) { + log_errorf("unable to activate '%s': %s", device->path, strerror(errno)); + } + } + + log_debugf("activated %zd devices", client->devices.length); + + seat->active_client = client; + if (client_enable_seat(client) == -1) { + seat_remove_client(seat, client); + return -1; + } + + log_info("client successfully enabled"); + return 0; +} + +int seat_close_client(struct seat *seat, struct client *client) { + assert(seat); + assert(client); + + if (seat->active_client != client) { + log_error("client not active"); + errno = EBUSY; + return -1; + } + + // We *deactivate* all remaining fds. These may later be reactivated. + // The reason we cannot just close them is that certain device fds, such + // as for DRM, must maintain the exact same file description for their + // contexts to remain valid. + for (size_t idx = 0; idx < client->devices.length; idx++) { + struct seat_device *device = client->devices.items[idx]; + if (seat_deactivate_device(client, device) == -1) { + log_errorf("unable to deactivate '%s': %s", device->path, strerror(errno)); + } + } + + log_debugf("deactivated %zd devices", client->devices.length); + + int vt = seat->active_client->seat_vt; + seat->active_client = NULL; + + if (seat->vt_bound) { + if (seat->vt_pending_ack) { + log_debug("acking pending VT switch"); + seat->vt_pending_ack = false; + terminal_teardown(vt); + terminal_ack_switch(); + return 0; + } + } + + seat_activate(seat); + log_debug("closed client"); + return 0; +} + +int seat_set_next_session(struct seat *seat, int session) { + assert(seat); + + // Check if the session number is valid + if (session <= 0) { + errno = EINVAL; + return -1; + } + + // Check if a switch is already queued + if (seat->next_vt > 0 || seat->next_client != NULL) { + return 0; + } + + struct client *target = NULL; + for (size_t idx = 0; idx < seat->clients.length; idx++) { + struct client *c = seat->clients.items[idx]; + if (client_get_session(c) == session) { + target = c; + break; + } + } + + if (target != NULL) { + log_info("queuing switch to different client"); + seat->next_client = target; + seat->next_vt = 0; + } else if (seat->vt_bound) { + log_info("queuing switch to different VT"); + seat->next_vt = session; + seat->next_client = NULL; + } else { + log_error("no valid switch available"); + errno = EINVAL; + return -1; + } + + if (client_disable_seat(seat->active_client) == -1) { + seat_remove_client(seat, seat->active_client); + } + + return 0; +} + +int seat_activate(struct seat *seat) { + assert(seat); + + // We already have an active client! + if (seat->active_client != NULL) { + return 0; + } + + // If we're asked to do a simple VT switch, do that + if (seat->vt_bound && seat->next_vt > 0) { + log_info("executing VT switch"); + terminal_switch_vt(seat->next_vt); + seat->next_vt = 0; + return 0; + } + + int vt = -1; + if (seat->vt_bound) { + vt = terminal_current_vt(); + } + + // Try to pick a client for activation + struct client *next_client = NULL; + if (seat->next_client != NULL) { + // A specific client has been requested, use it + next_client = seat->next_client; + seat->next_client = NULL; + } else if (seat->clients.length > 0 && seat->vt_bound) { + // No client is requested, try to find an applicable one + for (size_t idx = 0; idx < seat->clients.length; idx++) { + struct client *client = seat->clients.items[idx]; + if (client->seat_vt == vt) { + next_client = client; + break; + } + } + } else if (seat->clients.length > 0) { + next_client = seat->clients.items[0]; + } + + if (next_client == NULL) { + // No suitable client found + log_info("no client suitable for activation"); + if (seat->vt_bound) { + terminal_teardown(vt); + } + return -1; + } + + log_info("activating next client"); + if (seat->vt_bound && next_client->seat_vt != vt) { + terminal_switch_vt(next_client->seat_vt); + } + + return seat_open_client(seat, next_client); +} + +int seat_prepare_vt_switch(struct seat *seat) { + assert(seat); + + if (seat->active_client == NULL) { + log_info("no active client, performing switch immediately"); + terminal_ack_switch(); + return 0; + } + + if (seat->vt_pending_ack) { + log_info("impatient user, killing session to force pending switch"); + seat_close_client(seat, seat->active_client); + return 0; + } + + log_debug("delaying VT switch acknowledgement"); + + seat->vt_pending_ack = true; + if (client_disable_seat(seat->active_client) == -1) { + seat_remove_client(seat, seat->active_client); + } + + return 0; +} diff --git a/seatd/seatd.c b/seatd/seatd.c new file mode 100644 index 0000000..cfed341 --- /dev/null +++ b/seatd/seatd.c @@ -0,0 +1,57 @@ +#include <errno.h> +#include <poll.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/un.h> + +#include "client.h" +#include "log.h" +#include "poller.h" +#include "server.h" + +int main(int argc, char *argv[]) { + (void)argc; + (void)argv; + + char *loglevel = getenv("SEATD_LOGLEVEL"); + enum libseat_log_level level = LIBSEAT_ERROR; + if (loglevel != NULL) { + if (strcmp(loglevel, "silent") == 0) { + level = LIBSEAT_SILENT; + } else if (strcmp(loglevel, "info") == 0) { + level = LIBSEAT_INFO; + } else if (strcmp(loglevel, "debug") == 0) { + level = LIBSEAT_DEBUG; + } + } + libseat_log_init(level); + + struct server *server = server_create(); + if (server == NULL) { + log_errorf("server_create failed: %s", strerror(errno)); + return 1; + } + char *path = getenv("SEATD_SOCK"); + if (path == NULL) { + path = "/run/seatd.sock"; + } + + if (server_listen(server, path) == -1) { + log_errorf("server_listen failed: %s", strerror(errno)); + return 1; + } + + log_info("seatd started"); + + while (server->running) { + if (poller_poll(server->poller) == -1) { + log_errorf("poller failed: %s", strerror(errno)); + return 1; + } + } + unlink(path); + return 0; +} diff --git a/seatd/server.c b/seatd/server.c new file mode 100644 index 0000000..3db9b69 --- /dev/null +++ b/seatd/server.c @@ -0,0 +1,232 @@ +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <grp.h> +#include <signal.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/un.h> +#include <unistd.h> + +#include "client.h" +#include "list.h" +#include "log.h" +#include "poller.h" +#include "seat.h" +#include "server.h" +#include "terminal.h" + +#define LISTEN_BACKLOG 16 + +static int server_handle_vt_acq(int signal, void *data); +static int server_handle_vt_rel(int signal, void *data); +static int server_handle_kill(int signal, void *data); + +struct server *server_create(void) { + struct poller *poller = poller_create(); + if (poller == NULL) { + log_error("could not create poller"); + return NULL; + } + struct server *server = calloc(1, sizeof(struct server)); + if (server == NULL) { + return NULL; + } + server->poller = poller; + + list_init(&server->seats); + + if (poller_add_signal(poller, SIGUSR1, server_handle_vt_rel, server) == NULL || + poller_add_signal(poller, SIGUSR2, server_handle_vt_acq, server) == NULL || + poller_add_signal(poller, SIGINT, server_handle_kill, server) == NULL || + poller_add_signal(poller, SIGTERM, server_handle_kill, server) == NULL) { + server_destroy(server); + return NULL; + } + + char *vtenv = getenv("SEATD_VTBOUND"); + + // TODO: create more seats: + struct seat *seat = seat_create("seat0", vtenv == NULL || strcmp(vtenv, "1") == 0); + if (seat == NULL) { + server_destroy(server); + return NULL; + } + + list_add(&server->seats, seat); + server->running = true; + return server; +} + +void server_destroy(struct server *server) { + assert(server); + for (size_t idx = 0; idx < server->seats.length; idx++) { + struct seat *seat = server->seats.items[idx]; + seat_destroy(seat); + } + list_free(&server->seats); + if (server->poller != NULL) { + poller_destroy(server->poller); + server->poller = NULL; + } + free(server); +} + +struct seat *server_get_seat(struct server *server, const char *seat_name) { + for (size_t idx = 0; idx < server->seats.length; idx++) { + struct seat *seat = server->seats.items[idx]; + if (strcmp(seat->seat_name, seat_name) == 0) { + return seat; + } + } + return NULL; +} + +static int server_handle_vt_acq(int signal, void *data) { + (void)signal; + struct server *server = data; + struct seat *seat = server_get_seat(server, "seat0"); + if (seat == NULL) { + return -1; + } + + seat_activate(seat); + return 0; +} + +static int server_handle_vt_rel(int signal, void *data) { + (void)signal; + struct server *server = data; + struct seat *seat = server_get_seat(server, "seat0"); + if (seat == NULL) { + return -1; + } + + seat_prepare_vt_switch(seat); + return 0; +} + +static int server_handle_kill(int signal, void *data) { + (void)signal; + struct server *server = data; + server->running = false; + return 0; +} + +static int set_nonblock(int fd) { + int flags; + if ((flags = fcntl(fd, F_GETFD)) == -1 || fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) { + log_errorf("could not set FD_CLOEXEC on socket: %s", strerror(errno)); + return -1; + } + if ((flags = fcntl(fd, F_GETFL)) == -1 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { + log_errorf("could not set O_NONBLOCK on socket: %s", strerror(errno)); + return -1; + } + return 0; +} + +static int server_handle_connection(int fd, uint32_t mask, void *data) { + struct server *server = data; + if (mask & (EVENT_ERROR | EVENT_HANGUP)) { + close(fd); + log_errorf("server socket recieved an error: %s", strerror(errno)); + exit(1); + } + + if (mask & EVENT_READABLE) { + int new_fd = accept(fd, NULL, NULL); + if (fd == -1) { + log_errorf("could not accept client connection: %s", strerror(errno)); + return 0; + } + + if (set_nonblock(new_fd) != 0) { + close(new_fd); + log_errorf("could not prepare new client socket: %s", strerror(errno)); + return 0; + } + + struct client *client = client_create(server, new_fd); + client->event_source = poller_add_fd(server->poller, new_fd, EVENT_READABLE, + client_handle_connection, client); + if (client->event_source == NULL) { + client_destroy(client); + log_errorf("could not add client socket to poller: %s", strerror(errno)); + return 0; + } + log_infof("new client connected (pid: %d, uid: %d, gid: %d)", client->pid, + client->uid, client->gid); + } + return 0; +} + +int server_add_client(struct server *server, int fd) { + if (set_nonblock(fd) != 0) { + close(fd); + log_errorf("could not prepare new client socket: %s", strerror(errno)); + return -1; + } + + struct client *client = client_create(server, fd); + client->event_source = + poller_add_fd(server->poller, fd, EVENT_READABLE, client_handle_connection, client); + if (client->event_source == NULL) { + client_destroy(client); + log_errorf("could not add client socket to poller: %s", strerror(errno)); + return -1; + } + return 0; +} + +int server_listen(struct server *server, const char *path) { + union { + struct sockaddr_un unix; + struct sockaddr generic; + } addr = {0}; + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd == -1) { + log_errorf("could not create socket: %s", strerror(errno)); + return -1; + } + if (set_nonblock(fd) == -1) { + close(fd); + log_errorf("could not prepare socket: %s", strerror(errno)); + return -1; + } + + addr.unix.sun_family = AF_UNIX; + strncpy(addr.unix.sun_path, path, sizeof addr.unix.sun_path - 1); + socklen_t size = offsetof(struct sockaddr_un, sun_path) + strlen(addr.unix.sun_path); + if (bind(fd, &addr.generic, size) == -1) { + log_errorf("could not bind socket: %s", strerror(errno)); + close(fd); + return -1; + } + if (listen(fd, LISTEN_BACKLOG) == -1) { + log_errorf("could not listen on socket: %s", strerror(errno)); + close(fd); + return -1; + } + struct group *videogrp = getgrnam("video"); + if (videogrp != NULL) { + if (chown(path, 0, videogrp->gr_gid) == -1) { + log_errorf("could not chown socket to video group: %s", strerror(errno)); + } else if (chmod(path, 0770) == -1) { + log_errorf("could not chmod socket: %s", strerror(errno)); + } + } else { + log_errorf("could not get video group: %s", strerror(errno)); + } + if (poller_add_fd(server->poller, fd, EVENT_READABLE, server_handle_connection, server) == + NULL) { + log_errorf("could not add socket to poller: %s", strerror(errno)); + close(fd); + return -1; + } + return 0; +} |