/*
* secde -- experimental lightweight Wayland/X11 server
* Copyright (C) 2019 Samuel Lidén Borell
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#define _GNU_SOURCE
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "epoll.h"
#include "protohnd.h"
static int do_reload_config;
static struct Settings *settings;
/* evdev */
#define EVDEV_DIR "/dev/input"
#define MAXEVDEV 32 /* Max evdev device number */
static unsigned open_evdev_mask = 0; /* Assuming min 32 bit int on Linux */
static int open_evdevs[MAXEVDEV] = { 0 };
static int inputnfyfd; /* inotify fd */
/* epoll */
static int epollfd;
#define MAX_EVENTS 64
static struct epoll_event ev;
static struct epoll_event events[MAX_EVENTS];
#define QUEUE_SIZE (8*MAX_EVENTS)
static int readmore_count;
static FdInfo *readmore_fdinfo[QUEUE_SIZE]; /* = not fully read */
static unsigned char buff[1024];
static void add_evdev(const char *devname);
/*static void make_nonblocking(const int fd)
{
}*/
static void add_initial_evdev()
{
/* TODO add support for incremental scans, if the inotify queue overflows or if it's not supported? */
struct dirent *direntry;
DIR *dir = opendir(EVDEV_DIR);
if (!dir) {
perror("failed to open directory " EVDEV_DIR);
return;
}
for (;;) {
errno = 0;
direntry = readdir(dir);
if (!direntry) {
if (errno) {
perror("failed to read contents of directory " EVDEV_DIR);
}
break;
}
add_evdev(direntry->d_name);
}
closedir(dir);
}
static long strtoint(const char *str, long defval)
{
char *endp;
long value = strtol(str, &endp, 10);
return *endp ? defval : value;
}
static void add_evdev(const char *devname)
{
char path[11+NAME_MAX+1];
int fd, devnum, mask;
if (strncmp(devname, "event", 5)) return;
devnum = strtoint(&devname[5], -1);
if (devnum < 0 || devnum > MAXEVDEV) return;
mask = 1 << devnum;
if ((open_evdev_mask & mask) != 0) { /* TODO move this check to protohnd? e.g. add new function protohnd_serverexists(type, devnum); */
fprintf(stderr, "evdev device already open: %d\n", devnum);
return;
}
fprintf(stderr, "adding input device %d: " EVDEV_DIR "/%s\n", devnum, devname);
strcpy(path, EVDEV_DIR "/");
strncat(path, devname, NAME_MAX);
path[11+NAME_MAX] = '\0';
fd = open(path, O_CLOEXEC | O_NOCTTY | O_NONBLOCK | O_RDONLY);
if (fd == -1) {
perror(path);
return;
}
ev.events = EPOLLIN | EPOLLOUT | EPOLLET;
ev.data.fd = fd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) == -1) {
perror("'epoll_ctl' system call on input event device failed");
close(fd);
}
open_evdev_mask |= mask;
open_evdevs[devnum] = fd; /* TODO move this to protohnd? e.g. extend with new parameter protohnd_fdadded(fd, type, devnum) */
protohnd_fdadded(NULL, fd, FDTYPE_CHARDEV, FDP_EVDEV);
}
/** Adds a framebuffer device */
static void add_fbdev(const char *filename)
{
int fd = open(filename, O_CLOEXEC | O_NOCTTY | O_RDWR);
if (fd == -1) {
perror(filename);
return;
}
/* TODO can we use epoll to detect resolution changes? */
if (!protohnd_fdadded(NULL, fd, FDTYPE_CHARDEV, FDP_FBDEV)) {
close(fd);
return;
}
}
/** Handles an incoming connection */
static void do_accept(struct FdInfo *fdinfo, int fd)
{
int clientsock;
struct sockaddr_un clientaddr;
socklen_t clientaddrsize;
fprintf(stderr, "new connection\n");
clientsock = accept4(fd, (struct sockaddr *)&clientaddr,
&clientaddrsize, SOCK_CLOEXEC|SOCK_NONBLOCK);
if (clientsock == -1) {
if (errno == EAGAIN) {
fprintf(stderr, "not yet ready\n");
return;
}
perror("'accept' system call failed");
return;
}
/*make_nonblocking(clientsock);*/
fprintf(stderr, "conn added\n");
if (!protohnd_fdadded(fdinfo, clientsock,
FDTYPE_CLIENTSOCK, FDP_UNSPEC
/*, cliantaddr, clientaddrsize*/)) {
fprintf(stderr, "connection ignored\n");
return;
}
ev.events = EPOLLIN | EPOLLOUT | EPOLLET; /* FIXME race condition if data appears before this call? we should call read() once first */
ev.data.fd = clientsock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, clientsock, &ev) == -1) {
perror("'epoll_ctl' system call on server socket failed");
return;
}
}
/** Handles incoming data to read */
static void do_receive(struct FdInfo *fdinfo, int fd, FdFlags fdflags)
{
int numrecv;
int warned = 0;
next:
fprintf(stderr, "data to read\n");
if ((fdflags & FDF_SOCKET) != 0) {
numrecv = recv(fd, buff, sizeof(buff), 0);
} else {
numrecv = read(fd, buff, sizeof(buff));
}
if (numrecv == -1) {
if (errno == EAGAIN) {
fprintf(stderr, "not yet ready to read\n");
return;
} else if (errno == EINTR) {
goto next;
}
perror("'recv' or 'read' system call failed");
return;
} else if (numrecv != 0) {
/* Received data */
fprintf(stderr, "received %d bytes\n", numrecv);
if (fd != inputnfyfd) {
protohnd_received(fdinfo, buff, numrecv);
} else {
/* Special handling for inotify */
struct inotify_event infyev;
unsigned char *buffp = buff;
size_t remaining = numrecv;
printf("inotify read loop start\n");
while (remaining) {
if (remaining < sizeof(infyev)) {
fprintf(stderr, "truncated inotify event read!\n");
break;
}
memcpy(&infyev, buffp, sizeof(infyev));
/* TODO check for IN_Q_OVERFLOW, break and list directory contents? but that should be VERY unlikely to happen... */
buffp += sizeof(infyev);
remaining -= sizeof(infyev);
if (infyev.len < 1) {
fprintf(stderr, "invalid inotify filename\n");
break;
} else if (remaining < infyev.len) {
fprintf(stderr, "truncated inotify event read!\n");
break;
}
if (buffp[infyev.len-1] != '\0') {
fprintf(stderr, "invalid inotify filename\n");
break;
}
add_evdev((const char *)buffp);
buffp += infyev.len;
remaining -= infyev.len;
}
printf("inotify read loop end\n");
}
/* TODO only queue events if the full buffer was read
(or, for inotify, there is more free space in the
buffer than the max inotify data size) */
if (readmore_count >= QUEUE_SIZE) {
/* Queue is full, so we need to process the first item.
This can lead to priority inversion */
/* XXX untested, also might be slow with memmove */
FdInfo *first;
if (!warned) {
printf("'readmore' buffer full, untested code...\n");
}
first = readmore_fdinfo[0];
fd = protohnd_getfd(first);
fdflags = protohnd_getfdflags(first);
memmove(&readmore_fdinfo[0], &readmore_fdinfo[1],
sizeof(readmore_fdinfo) - sizeof(void*));
readmore_fdinfo[QUEUE_SIZE-1] = fdinfo;
goto next;
} else {
readmore_fdinfo[readmore_count++] = fdinfo;
}
} else {
/* Connection closed */
/* TODO clear out open_evdev_mask and open_evdevs[evdev]==fd */
close(fd);
protohnd_closed(fdinfo);
}
}
/**
* Handles SIGHUP.
*/
static void sighup_handler(int signum)
{
/* All function calls here must be async-signal-safe! */
static const char message[] = "Received SIGHUP. A reload has been requested.\n";
(void) signum;
write(STDOUT_FILENO, message, sizeof(message)-1);
do_reload_config = 1;
}
/**
* Scans for new devices (useful if inotify does not
* work), and will reload any configuration in the future if added.
*/
static void reload_config()
{
do_reload_config = 0;
printf("Handling reload requested by SIGHUP. Reloading devices.\n");
add_initial_evdev();
}
static void install_reload_handler()
{
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = sighup_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESTART;
if (sigaction(SIGHUP, &act, NULL) == -1) {
perror("failed to install signal handler for reload");
}
}
/**
* Runs at process exit. Removes socket files.
*/
static void cleanup()
{
/* All function calls here must be async-signal-safe! */
unlink(settings->x11sockfile);
}
/**
* Handles SIGTERM and SIGINT. Runs cleanup
*/
static void sigtermint_handler(int signum)
{
/* All function calls here must be async-signal-safe! */
cleanup();
signal(signum, SIG_DFL);
raise(signum);
}
static void install_cleanup_handler()
{
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = sigtermint_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESETHAND;
if (sigaction(SIGTERM, &act, NULL) == -1) {
perror("failed to install signal handler for cleanup");
}
if (sigaction(SIGINT, &act, NULL) == -1) {
perror("failed to install signal handler for cleanup");
}
atexit(cleanup);
}
int mainloop_epoll(struct Settings *_settings)
{
int serversock;
struct sockaddr_un serveraddr;
settings = _settings;
protohnd_initing();
install_cleanup_handler();
/* X11 socket */
serversock = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC/*|SOCK_NONBLOCK*/, PF_UNIX);
if (serversock == -1) {
perror("Creating X11 server socket failed");
return EXIT_FAILURE;
}
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sun_family = AF_UNIX;
strncpy(serveraddr.sun_path, settings->x11sockfile,
sizeof(serveraddr.sun_path)-1);
if (bind(serversock, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) {
perror("Binding X11 server socket failed");
fprintf(stderr, "Perhaps there is already an X11 server at %s\n",
settings->x11sockfile);
return EXIT_FAILURE;
}
if (listen(serversock, 511) == -1) {
perror("Listening on X11 server socket failed");
return EXIT_FAILURE;
}
protohnd_fdadded(NULL, serversock, FDTYPE_SERVERSOCK, FDP_X11);
/* KMS fd */
/* TODO */
/* FB fd - if KMS is unavailable */
/* TODO can fb devices be created dynamically,
e.g. when an external monitor is connected? */
add_fbdev("/dev/fb0");
/* epoll for connections and data */
epollfd = epoll_create1(EPOLL_CLOEXEC);
if (epollfd == -1) {
perror("'epoll_create1' system call failed");
return EXIT_FAILURE;
}
ev.events = EPOLLIN;
ev.data.fd = serversock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, serversock, &ev) == -1) {
perror("'epoll_ctl' system call on server socket failed");
return EXIT_FAILURE;
}
/* EVDEV change notification */
inputnfyfd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
if (inputnfyfd == -1) {
perror("inotify_init1 failed");
} else {
/* devices might be created as root:root first, and then chown'ed,
so we need to check for file attribute modifications also */
int watch = inotify_add_watch(inputnfyfd, EVDEV_DIR, IN_CREATE | IN_ATTRIB);
if (watch == -1) {
perror("Failed to add watch for " EVDEV_DIR " changes");
close(inputnfyfd);
inputnfyfd = -1;
}
}
if (inputnfyfd != -1) {
ev.events = EPOLLIN;
ev.data.fd = inputnfyfd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, inputnfyfd, &ev) == -1) {
perror("'epoll_ctl' system call on inotify fd failed");
close(inputnfyfd);
} else {
protohnd_fdadded(NULL, inputnfyfd,
FDF_READABLE, FDP_INOTIFY);
}
}
/* EVDEV initialization */
add_initial_evdev();
/* TODO /usr/include/linux/input.h, /usr/include/linux/input-event-codes.h, (/usr/include/linux/uinput.h?) */
install_reload_handler();
protohnd_inited();
for (;;) {
int nfds, i;
/* Check for reload (SIGHUP) */
if (do_reload_config) {
reload_config();
}
/* Check for events */
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
if (errno == EINTR) continue;
perror("'epoll_wait' system call failed");
return EXIT_FAILURE;
}
readmore_count = 0;
for (i = 0; i < nfds; i++) {
int fd = events[i].data.fd;
struct FdInfo *fdinfo = protohnd_getfdinfo(fd);
FdFlags fdflags = protohnd_getfdflags(fdinfo);
if ((fdflags & FDF_SERVER) != 0) {
/* New connection */
do_accept(fdinfo, fd);
} else {
/* XXX should we handle EPOLLRDHUP? */
if (events[i].events & EPOLLHUP) {
protohnd_willclose(fdinfo);
}
if (events[i].events & EPOLLIN) {
/* Received data */
do_receive(fdinfo, fd, fdflags);
}
if (events[i].events & EPOLLOUT) {
/* Ready to write */
protohnd_writeready(fdinfo, 1);
fprintf(stderr, "ready to write\n");
}
}
}
/* Check if there is data to write */
for (;;) {
struct FdInfo *fdinfo;
int fd;
FdFlags fdflags;
unsigned char *buffp = buff;
int bufflen = sizeof(buff);
int hasmore = 0;
int numsent;
fdinfo = protohnd_getwrite(&buffp, &bufflen, &hasmore);
if (!fdinfo) break; /* nothing to write */
fd = protohnd_getfd(fdinfo);
fdflags = protohnd_getfdflags(fdinfo);
retry_write:
if ((fdflags & FDF_SOCKET) != 0) {
numsent = send(fd, buffp, bufflen, hasmore ? MSG_MORE : 0);
} else {
numsent = write(fd, buffp, bufflen);
}
if (numsent > 0) {
int remaining = bufflen - numsent;
protohnd_written(fdinfo, numsent, remaining, 1);
} else if (numsent == 0) {
protohnd_written(fdinfo, 0, bufflen, 1);
close(fd);
protohnd_closed(fdinfo);
} else if (errno == EAGAIN) {
protohnd_writeready(fdinfo, 0);
protohnd_written(fdinfo, 0, bufflen, -1);
} else if (errno == EINTR) {
goto retry_write;
} else {
perror("'send' or 'write' system call failed");
return EXIT_FAILURE;
}
}
/* Process queued reads */
for (i = 0; i < readmore_count; i++) {
FdInfo *fdinfo = readmore_fdinfo[i];
int fd = protohnd_getfd(fdinfo);
FdFlags fdflags = protohnd_getfdflags(fdinfo);
do_receive(fdinfo, fd, fdflags);
}
}
return 0;
}