seatd/seatd/seat.c
2020-11-23 17:56:55 +01:00

647 lines
15 KiB
C

#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 "linked_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;
}
linked_list_init(&seat->clients);
seat->vt_bound = vt_bound;
seat->seat_name = strdup(seat_name);
seat->cur_vt = 0;
seat->cur_ttyfd = -1;
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 (!linked_list_empty(&seat->clients)) {
struct client *client = (struct client *)seat->clients.next;
assert(client->seat == seat);
client_destroy(client);
}
assert(seat->cur_ttyfd == -1);
linked_list_remove(&seat->link);
free(seat->seat_name);
free(seat);
}
static void seat_update_vt(struct seat *seat) {
int tty0fd = terminal_open(0);
if (tty0fd == -1) {
log_errorf("unable to open tty0: %s", strerror(errno));
return;
}
seat->cur_vt = terminal_current_vt(tty0fd);
close(tty0fd);
}
static int vt_open(struct seat *seat, int vt) {
assert(vt != -1);
if (seat->cur_ttyfd != -1) {
terminal_set_process_switching(seat->cur_ttyfd, true);
close(seat->cur_ttyfd);
}
seat->cur_ttyfd = terminal_open(vt);
if (seat->cur_ttyfd == -1) {
log_errorf("could not open terminal for vt %d: %s", vt, strerror(errno));
return -1;
}
terminal_set_process_switching(seat->cur_ttyfd, true);
terminal_set_keyboard(seat->cur_ttyfd, false);
terminal_set_graphics(seat->cur_ttyfd, true);
return 0;
}
static void vt_close(struct seat *seat) {
if (seat->cur_ttyfd == -1) {
return;
}
terminal_set_process_switching(seat->cur_ttyfd, true);
terminal_set_keyboard(seat->cur_ttyfd, true);
terminal_set_graphics(seat->cur_ttyfd, false);
close(seat->cur_ttyfd);
seat->cur_ttyfd = -1;
}
static int vt_switch(struct seat *seat, int vt) {
int ttyfd = terminal_open(seat->cur_vt);
if (ttyfd == -1) {
log_errorf("could not open terminal: %s", strerror(errno));
return -1;
}
terminal_set_process_switching(ttyfd, true);
terminal_switch_vt(ttyfd, vt);
close(ttyfd);
return 0;
}
static int vt_ack(struct seat *seat, bool release) {
int tty0fd = terminal_open(seat->cur_vt);
if (tty0fd == -1) {
log_errorf("unable to open tty0: %s", strerror(errno));
return -1;
}
if (release) {
terminal_ack_release(tty0fd);
} else {
terminal_ack_acquire(tty0fd);
}
close(tty0fd);
return 0;
}
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;
}
if (client->session != -1) {
log_error("cannot add client: client cannot be reused");
return -1;
}
if (seat->vt_bound) {
seat_update_vt(seat);
if (seat->cur_vt == -1) {
log_error("could not determine VT for client");
return -1;
}
client->session = seat->cur_vt;
} else {
client->session = seat->session_cnt++;
}
log_debugf("registered client %p as session %d", (void *)client, client->session);
client->seat = seat;
log_debug("added client");
return 0;
}
int seat_remove_client(struct client *client) {
assert(client);
assert(client->seat);
struct seat *seat = client->seat;
if (seat->next_client == client) {
seat->next_client = NULL;
}
while (!linked_list_empty(&client->devices)) {
struct seat_device *device = (struct seat_device *)client->devices.next;
seat_close_device(client, device);
}
if (seat->active_client == client) {
seat_close_client(client);
}
client->seat = NULL;
log_debug("removed client");
return 0;
}
struct seat_device *seat_find_device(struct client *client, int device_id) {
assert(client);
assert(client->seat);
assert(device_id != 0);
for (struct linked_list *elem = client->devices.next; elem != &client->devices;
elem = elem->next) {
struct seat_device *seat_device = (struct seat_device *)elem;
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;
}
if (client->pending_disable) {
errno = EPERM;
return NULL;
}
char sanitized_path[PATH_MAX];
if (realpath(path, sanitized_path) == NULL) {
log_errorf("invalid path '%s': %s", path, strerror(errno));
return NULL;
}
enum seat_device_type type;
if (path_is_evdev(sanitized_path)) {
type = SEAT_DEVICE_TYPE_EVDEV;
} else if (path_is_drm(sanitized_path)) {
type = SEAT_DEVICE_TYPE_DRM;
} else {
log_errorf("invalid path '%s'", sanitized_path);
errno = ENOENT;
return NULL;
}
int device_id = 1;
size_t device_count = 0;
struct seat_device *device = NULL;
for (struct linked_list *elem = client->devices.next; elem != &client->devices;
elem = elem->next) {
struct seat_device *old_device = (struct seat_device *)elem;
if (strcmp(old_device->path, sanitized_path) == 0) {
old_device->ref_cnt++;
device = old_device;
goto done;
}
if (old_device->device_id >= device_id) {
device_id = old_device->device_id + 1;
}
device_count++;
}
if (device_count >= MAX_SEAT_DEVICES) {
log_error("max seat devices exceeded");
errno = EMFILE;
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;
}
switch (type) {
case SEAT_DEVICE_TYPE_DRM:
if (drm_set_master(fd) == -1) {
log_debugf("drm_set_master failed: %s", strerror(errno));
}
break;
case SEAT_DEVICE_TYPE_EVDEV:
// Nothing to do here
break;
default:
log_error("invalid seat device type");
abort();
}
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;
}
device->ref_cnt = 1;
device->type = type;
device->fd = fd;
device->device_id = device_id;
device->active = true;
linked_list_insert(&client->devices, &device->link);
done:
log_debugf("seat: %p, client: %p, path: '%s', device_id: %d, ref_cnt: %d", (void *)seat,
(void *)client, path, device_id, device->ref_cnt);
return device;
}
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;
}
switch (seat_device->type) {
case SEAT_DEVICE_TYPE_DRM:
if (drm_drop_master(seat_device->fd) == -1) {
log_debugf("drm_drop_master failed: %s", strerror(errno));
return -1;
}
break;
case SEAT_DEVICE_TYPE_EVDEV:
if (evdev_revoke(seat_device->fd) == -1) {
log_debugf("evdev_revoke failed: %s", strerror(errno));
return -1;
}
break;
default:
log_error("invalid seat device type");
abort();
}
seat_device->active = false;
return 0;
}
int seat_close_device(struct client *client, struct seat_device *seat_device) {
assert(client);
assert(client->seat);
assert(seat_device && seat_device->fd != -1);
log_debugf("seat: %p, client: %p, path: '%s', device_id: %d, ref_cnt: %d",
(void *)client->seat, (void *)client, seat_device->path, seat_device->device_id,
seat_device->ref_cnt);
seat_device->ref_cnt--;
if (seat_device->ref_cnt > 0) {
return 0;
}
linked_list_remove(&seat_device->link);
if (seat_device->fd != -1) {
seat_deactivate_device(client, seat_device);
close(seat_device->fd);
}
free(seat_device->path);
free(seat_device);
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;
}
switch (seat_device->type) {
case SEAT_DEVICE_TYPE_DRM:
if (drm_set_master(seat_device->fd) == -1) {
log_debugf("drmset_master failed: %s", strerror(errno));
}
seat_device->active = true;
break;
case SEAT_DEVICE_TYPE_EVDEV:
errno = EINVAL;
return -1;
default:
log_error("invalid seat device type");
abort();
}
return 0;
}
static int seat_activate(struct seat *seat) {
assert(seat);
if (seat->active_client != NULL) {
return 0;
}
struct client *next_client = NULL;
if (seat->next_client != NULL) {
log_info("activating next queued client");
next_client = seat->next_client;
seat->next_client = NULL;
} else if (linked_list_empty(&seat->clients)) {
log_info("no clients on seat to activate");
return -1;
} else if (seat->vt_bound) {
for (struct linked_list *elem = seat->clients.next; elem != &seat->clients;
elem = elem->next) {
struct client *client = (struct client *)elem;
if (client->session == seat->cur_vt) {
log_infof("activating client belonging to VT %d", seat->cur_vt);
next_client = client;
goto done;
}
}
log_infof("no clients belonging to VT %d to activate", seat->cur_vt);
return -1;
} else {
log_info("activating first client on seat");
next_client = (struct client *)seat->clients.next;
}
done:
return seat_open_client(seat, next_client);
}
int seat_open_client(struct seat *seat, struct client *client) {
assert(seat);
assert(client);
assert(!client->pending_disable);
if (seat->active_client != NULL) {
log_error("client already active");
errno = EBUSY;
return -1;
}
if (seat->vt_bound && vt_open(seat, client->session) == -1) {
goto error;
}
for (struct linked_list *elem = client->devices.next; elem != &client->devices;
elem = elem->next) {
struct seat_device *device = (struct seat_device *)elem;
if (seat_activate_device(client, device) == -1) {
log_errorf("unable to activate '%s': %s", device->path, strerror(errno));
}
}
seat->active_client = client;
if (client_send_enable_seat(client) == -1) {
log_error("could not send enable signal");
goto error;
}
log_info("client successfully enabled");
return 0;
error:
if (seat->vt_bound) {
vt_close(seat);
}
return -1;
}
int seat_close_client(struct client *client) {
assert(client);
assert(client->seat);
struct seat *seat = client->seat;
if (seat->active_client != client) {
log_error("client not active");
errno = EBUSY;
return -1;
}
while (!linked_list_empty(&client->devices)) {
struct seat_device *device = (struct seat_device *)client->devices.next;
if (seat_close_device(client, device) == -1) {
log_errorf("unable to close '%s': %s", device->path, strerror(errno));
}
}
client->pending_disable = false;
seat->active_client = NULL;
log_debug("closed client");
seat_activate(seat);
if (seat->vt_bound && seat->active_client == NULL) {
vt_close(seat);
}
return 0;
}
static int seat_disable_client(struct client *client) {
assert(client);
assert(client->seat);
struct seat *seat = client->seat;
if (seat->active_client != client) {
log_error("client not active");
errno = EBUSY;
return -1;
}
if (client->pending_disable) {
log_error("client already pending disable");
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 (struct linked_list *elem = client->devices.next; elem != &client->devices;
elem = elem->next) {
struct seat_device *device = (struct seat_device *)elem;
if (seat_deactivate_device(client, device) == -1) {
log_errorf("unable to deactivate '%s': %s", device->path, strerror(errno));
}
}
client->pending_disable = true;
if (client_send_disable_seat(seat->active_client) == -1) {
log_error("could not send disable event");
return -1;
}
log_debug("disabling client");
return 0;
}
int seat_ack_disable_client(struct client *client) {
assert(client);
assert(client->seat);
struct seat *seat = client->seat;
if (!client->pending_disable) {
log_error("client not pending disable");
errno = EBUSY;
return -1;
}
client->pending_disable = false;
log_debug("disabled client");
if (seat->active_client != client) {
return 0;
}
seat->active_client = NULL;
seat_activate(seat);
if (seat->vt_bound && seat->active_client == NULL) {
vt_close(seat);
}
return 0;
}
int seat_set_next_session(struct client *client, int session) {
assert(client);
assert(client->seat);
struct seat *seat = client->seat;
if (seat->active_client != client || client->pending_disable) {
log_error("client not active or pending disable");
errno = EPERM;
return -1;
}
if (session <= 0) {
log_errorf("invalid session value: %d", session);
errno = EINVAL;
return -1;
}
if (session == client->session) {
log_info("requested session is already active");
return 0;
}
if (seat->next_client != NULL) {
log_info("switch is already queued");
return 0;
}
if (seat->vt_bound) {
log_infof("switching to VT %d from %d", session, seat->cur_vt);
if (vt_switch(seat, session) == -1) {
log_error("could not switch VT");
return -1;
}
return 0;
}
struct client *target = NULL;
for (struct linked_list *elem = seat->clients.next; elem != &seat->clients;
elem = elem->next) {
struct client *c = (struct client *)elem;
if (c->session == session) {
target = c;
break;
}
}
if (target == NULL) {
log_error("no valid switch available");
errno = EINVAL;
return -1;
}
log_infof("queuing switch client with session %d", session);
seat->next_client = target;
seat_disable_client(seat->active_client);
return 0;
}
int seat_vt_activate(struct seat *seat) {
assert(seat);
if (!seat->vt_bound) {
log_debug("VT activation on non VT-bound seat, ignoring");
return -1;
}
seat_update_vt(seat);
log_debug("activating VT");
vt_ack(seat, false);
if (seat->active_client == NULL) {
seat_activate(seat);
}
return 0;
}
int seat_vt_release(struct seat *seat) {
assert(seat);
if (!seat->vt_bound) {
log_debug("VT release request on non VT-bound seat, ignoring");
return -1;
}
seat_update_vt(seat);
log_debug("releasing VT");
if (seat->active_client != NULL) {
seat_disable_client(seat->active_client);
}
vt_ack(seat, true);
seat->cur_vt = -1;
return 0;
}