Skip to content

Commit

Permalink
Support input device hot-plugging and removal via udev
Browse files Browse the repository at this point in the history
udev (userspace /dev) is a device manager for the Linux kernel. By monitoring
"add" and "remove" events from udev, we can dynamically update the event device
file descriptors table to enable device hot-plugging and removal.

Close sysprog21#61
  • Loading branch information
shengwen-tw committed Nov 9, 2024
1 parent efe7d49 commit cedf03e
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 13 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ endif

ifeq ($(CONFIG_BACKEND_FBDEV), y)
BACKEND = fbdev
TARGET_LIBS += -ludev
libtwin.a_files-y += backend/fbdev.c
libtwin.a_files-y += backend/linux_input.c
endif
Expand Down
134 changes: 121 additions & 13 deletions backend/linux_input.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

#include <fcntl.h>
#include <libudev.h>
#include <linux/input.h>
#include <poll.h>
#include <pthread.h>
Expand All @@ -18,18 +19,21 @@

#define EVDEV_CNT_MAX 32
#define EVDEV_NAME_SIZE_MAX 50
#define UDEV_RESERVED_CNT 1

#define UDEV_EVDEV_FD_CNT (EVDEV_CNT_MAX + UDEV_RESERVED_CNT)

typedef struct {
twin_screen_t *screen;
pthread_t evdev_thread;
int fd;
int btns;
int x, y;
int evdev_fd[EVDEV_CNT_MAX];
size_t evdev_cnt;
size_t udev_cnt;
} twin_linux_input_t;

static int evdev_fd[EVDEV_CNT_MAX];
static int evdev_cnt;

static void check_mouse_bounds(twin_linux_input_t *tm)
{
if (tm->x < 0)
Expand Down Expand Up @@ -101,37 +105,137 @@ static void twin_linux_input_events(struct input_event *ev,
}
}

static void *twin_linux_evdev_thread(void *arg)
static int twin_linux_udev_init(struct udev **udev, struct udev_monitor **mon)
{
twin_linux_input_t *tm = arg;
/* Create udev object */
*udev = udev_new();
if (!*udev) {
log_error("Failed to create udev object");
return -1;
}

/* Create a monitor for kernel events */
*mon = udev_monitor_new_from_netlink(*udev, "udev");
if (!*mon) {
log_error("Failed to create udev monitor");
udev_unref(*udev);
return -1;
}

/* Filter for input subsystem */
udev_monitor_filter_add_match_subsystem_devtype(*mon, "input", NULL);
udev_monitor_enable_receiving(*mon);

/* File descriptor for the monitor */
return udev_monitor_get_fd(*mon);
}

static bool twin_linux_udev_update(struct udev_monitor *mon)
{
struct udev_device *dev = NULL;

/* Get the device that caused the event */
dev = udev_monitor_receive_device(mon);
if (dev) {
const char *action = udev_device_get_action(dev);
const char *dev_node = udev_device_get_devnode(dev);

if (action && dev_node) {
const char *keyboard =
udev_device_get_property_value(dev, "ID_INPUT_KEYBOARD");
const char *mouse =
udev_device_get_property_value(dev, "ID_INPUT_MOUSE");

/* Ensure udev event is for mouse or keyboard */
if (!keyboard && !mouse) {
udev_device_unref(dev);
return false;
}

/* Capture only add and remove events */
if (!strcmp(action, "add") || !strcmp(action, "remove")) {
log_info("udev: %s: %s", action, dev_node);
udev_device_unref(dev);
return true;
}
}
}

/* No event is caputured */
return false;
}

static void twin_linux_edev_open(struct pollfd *pfds, twin_linux_input_t *tm)
{
/* Reset evdev fd table */
for (size_t i = 0; i < tm->evdev_cnt; i++) {
close(tm->evdev_fd[i]);
}
tm->evdev_cnt = 0;

/* Open all event devices */
char evdev_name[EVDEV_NAME_SIZE_MAX] = {0};
for (int i = 0; i < EVDEV_CNT_MAX; i++) {
snprintf(evdev_name, EVDEV_NAME_SIZE_MAX, "/dev/input/event%d", i);
int fd = open(evdev_name, O_RDWR | O_NONBLOCK);
if (fd > 0) {
evdev_fd[evdev_cnt] = fd;
evdev_cnt++;
tm->evdev_fd[tm->evdev_cnt] = fd;
tm->evdev_cnt++;
}
}

/* Initialize pollfd array */
struct pollfd pfds[EVDEV_CNT_MAX];
for (int i = 0; i < evdev_cnt; i++) {
pfds[i].fd = evdev_fd[i];
/* Initialize evdev poll file descriptors */
for (size_t i = tm->udev_cnt; i < tm->evdev_cnt + tm->udev_cnt; i++) {
pfds[i].fd = tm->evdev_fd[i - 1];
pfds[i].events = POLLIN;
}
}

static void *twin_linux_evdev_thread(void *arg)
{
twin_linux_input_t *tm = arg;

struct udev *udev = NULL;
struct udev_monitor *mon = NULL;

/* Open Linux udev (user space device manager) */
int udev_fd = twin_linux_udev_init(&udev, &mon);
if (udev_fd >= 0)
tm->udev_cnt = 1;

/* Place the udev fd into the poll fds */
struct pollfd pfds[UDEV_EVDEV_FD_CNT];
pfds[0].fd = udev_fd;
pfds[0].events = POLLIN;

/* Open event devices */
twin_linux_edev_open(pfds, tm);

/* Accessing to input devices is impossible, terminate the thread */
if (tm->evdev_cnt == 0 && tm->udev_cnt == 0) {
log_error("Failed to open udev and evdev");
pthread_exit(NULL);
}

/* Event polling */
struct input_event ev;
while (1) {
/* Wait until any event is available */
if (poll(pfds, evdev_cnt, -1) <= 0)
if (poll(pfds, tm->evdev_cnt + tm->udev_cnt, -1) <= 0)
continue;

/* Iterate through all file descriptors */
for (int i = 0; i < evdev_cnt; i++) {
for (size_t i = 0; i < tm->evdev_cnt + tm->udev_cnt; i++) {
if (i < tm->udev_cnt) {
/* Check udev event */
if (twin_linux_udev_update(mon)) {
/* Re-open event devices */
twin_linux_edev_open(pfds, tm);
break;
}
continue;
}

/* Try reading events */
ssize_t n = read(pfds[i].fd, &ev, sizeof(ev));
if (n == sizeof(ev)) {
Expand All @@ -141,6 +245,10 @@ static void *twin_linux_evdev_thread(void *arg)
}
}

/* Clean up */
udev_monitor_unref(mon);
udev_unref(udev);

return NULL;
}

Expand Down

0 comments on commit cedf03e

Please sign in to comment.