aboutsummaryrefslogtreecommitdiff
path: root/seatd/seat.c
diff options
context:
space:
mode:
Diffstat (limited to 'seatd/seat.c')
-rw-r--r--seatd/seat.c521
1 files changed, 521 insertions, 0 deletions
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;
+}