/* * 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; }