diff --git a/README.md b/README.md index 6105d9d..3bf3882 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ libudev-zero ============ -Drop-in replacement for libudev intended to work without daemon +Drop-in replacement for libudev enables you to use whatever +device manager you like without worrying about udev dependency at all! What Works ---------- + * [x] xorg-server * [x] libinput * [x] wlroots * [x] weston -* [ ] libusb - doesn't work for now. temporary workaround is compiling libusb with --disable-udev +* [x] libusb * [x] kwin - [fix](https://github.com/dilyn-corner/KISS-kde/commit/0cc72748e46f859a0fced55b0c3fcc1dd9586a38) * [ ] ??? @@ -19,6 +21,7 @@ Dependencies * C99 compiler (build time) * POSIX make (build time) * POSIX & XSI libc +* epoll & inotify * Linux >= 2.6.39 Installation @@ -31,12 +34,51 @@ make PREFIX=/usr install # will overwrite existing udev libraries if any # here we go ! ``` +Hotplugging +----------- + +There is no complicated or overengineered way to use hotplugging. Everything is +portable as much as possible. To use hotplugging the only thing you need is +uevent's receiver (device manager, busybox `uevent`, CONFIG_UEVENT_HELPER, ...). +I will describe only mdev and CONFIG_UEVENT_HELPER because their usage is very basic. +For busybox `uevent` you need to write your own parser which is kinda ... complex. + +UDEV_MONITOR_DIR is arbitrary directory where uevent files stored. +Default is `/tmp/.libudev-zero`. You can change it at build time by appending +`-DUDEV_MONITOR_DIR=` to CFLAGS. I don't recommend setting UDEV_MONITOR_DIR +to regular fs (i.e non-tmpfs) because unneeded files aren't automatically discarded +after reboot or termination (yet). + +* mdev + + - merge [mdev.conf](contrib/mdev.conf) with your mdev.conf + - restart mdev daemon + +* CONFIG_UEVENT_HELPER + + - ensure that CONFIG_UEVENT_HELPER enabled in kernel + - add full path of [helper.sh](contrib/helper.sh) (must be executable) or + [helper.c](contrib/helper.c) (compile it first) to /proc/sys/kernel/hotplug + + example: + ```sh + echo /full/path/to/helper > /proc/sys/kernel/hotplug # will use default UDEV_MONITOR_DIR + OR + echo "/full/path/to/helper " > /proc/sys/kernel/hotplug # change to your UDEV_MONITOR_DIR + ``` + +* run application which uses hotplugging (e.g xorg-server) +* unplug and plug something to test working capacity + +That's all! If you realized that this doesn't work for you, +you can always open an issue and describe your bug. + TODO ---- * [x] speed up performance * [x] extend devices support -* [ ] implement hotplugging support +* [x] implement hotplugging support Donate ------ diff --git a/contrib/helper.c b/contrib/helper.c new file mode 100644 index 0000000..ba46094 --- /dev/null +++ b/contrib/helper.c @@ -0,0 +1,63 @@ +/* + * this helper pretty similar to helper.sh, but it + * doesn't write unrelated variables to file (e.g PWD or PATH) + * + * build: + * cc helper.c -o helper + * + * usage: + * echo /full/path/to/helper > /proc/sys/kernel/hotplug + * echo "/full/path/to/helper UDEV_MONITOR_DIR" > /proc/sys/kernel/hotplug + */ + +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + extern char **environ; + char path[PATH_MAX]; + char *dir; + int fd, i; + + switch (argc) { + case 1: + dir = "/tmp/.libudev-zero"; + break; + case 2: + dir = argv[1]; + break; + default: + fprintf(stderr, "usage: %s [dir]\n", argv[0]); + return 1; + } + + snprintf(path, sizeof(path), "%s/uevent.XXXXXX", dir); + fd = mkstemp(path); + + if (fd == -1) { + perror("mkstemp"); + return 1; + } + + for (i = 0; environ[i]; i++) { + if (strncmp(environ[i], "PATH", 4) == 0 || + strncmp(environ[i], "HOME", 4) == 0) { + continue; + } + + if (write(fd, environ[i], strlen(environ[i])) == -1 || + write(fd, "\n", 1) == -1) { + perror("write"); + close(fd); + unlink(path); + return 1; + } + } + + close(fd); + return 0; +} diff --git a/contrib/helper.sh b/contrib/helper.sh new file mode 100644 index 0000000..ef9877a --- /dev/null +++ b/contrib/helper.sh @@ -0,0 +1,13 @@ +#!/bin/sh -f +# +# this helper required for CONFIG_UEVENT_HELPER aka /proc/sys/kernel/hotplug +# for mdev you can use simple one-liner, refer to mdev.conf for more info +# +# usage: +# echo /full/path/to/helper.sh > /proc/sys/kernel/hotplug +# echo "/full/path/to/helper.sh UDEV_MONITOR_DIR" > /proc/sys/kernel/hotplug +# + +# NOTE: writing variables to file (e.g PWD or PATH) +# that are not related to uevent properties is harmless +env > "${1:-/tmp/.libudev-zero}/uevent.$$" diff --git a/contrib/mdev.conf b/contrib/mdev.conf new file mode 100644 index 0000000..ff5334c --- /dev/null +++ b/contrib/mdev.conf @@ -0,0 +1,13 @@ +# +# example basic mdev config representing libudev-zero usage +# uncomment needed rules +# +# NOTE: you must change "/tmp/.libudev-zero" if you use non-default UDEV_MONITOR_DIR +# NOTE: mdev can only handle ADD and/or REMOVE events + +# will handle all uevents +#-.* root:root 660 *env > /tmp/.libudev-zero/uevent.$$ + +# will handle only drm and input uevents (recommended) +#SUBSYSTEM=drm;.* root:video 660 *env > /tmp/.libudev-zero/uevent.$$ +#SUBSYSTEM=input;.* root:input 660 *env > /tmp/.libudev-zero/uevent.$$ diff --git a/udev.h b/udev.h index b8b2454..08188ea 100644 --- a/udev.h +++ b/udev.h @@ -96,6 +96,9 @@ struct udev_hwdb *udev_hwdb_ref(struct udev_hwdb *hwdb); struct udev_hwdb *udev_hwdb_unref(struct udev_hwdb *hwdb); struct udev_list_entry *udev_hwdb_get_properties_list_entry(struct udev_hwdb *hwdb, const char *modalias, unsigned int flags); +// this is "libudev-zero" extension. do not use if portability is concern +struct udev_device *udev_device_new_from_file(struct udev *udev, const char *path); + #ifdef __cplusplus } #endif diff --git a/udev_device.c b/udev_device.c index e059ad3..bebfd2c 100644 --- a/udev_device.c +++ b/udev_device.c @@ -9,7 +9,7 @@ #include "udev.h" #include "udev_list.h" -enum { BITS_SIZE = 96 }; +enum { BITS_MAX = 96 }; struct udev_device { struct udev_list_entry properties; @@ -148,7 +148,7 @@ int udev_device_get_is_initialized(struct udev_device *udev_device) const char *udev_device_get_action(struct udev_device *udev_device) { - return NULL; + return udev_device_get_property_value(udev_device, "ACTION"); } int udev_device_has_tag(struct udev_device *udev_device, const char *tag) @@ -193,7 +193,9 @@ const char *udev_device_get_sysattr_value(struct udev_device *udev_device, const struct udev_list_entry *list_entry; char data[BUFSIZ], path[PATH_MAX]; struct stat st; + size_t len; FILE *file; + char *pos; if (!udev_device || !sysattr) { return NULL; @@ -217,13 +219,21 @@ const char *udev_device_get_sysattr_value(struct udev_device *udev_device, const return NULL; } - if (fread(data, 1, sizeof(data), file) != sizeof(data) && ferror(file)) { + len = fread(data, 1, sizeof(data), file); + + if (len != sizeof(data) && ferror(file)) { fclose(file); return NULL; } + if ((pos = memchr(data, '\n', len))) { + *pos = '\0'; + } + else { + data[len] = '\0'; + } + fclose(file); - data[strcspn(data, "\n")] = '\0'; list_entry = udev_list_entry_add(&udev_device->sysattrs, sysattr, data, 0); return udev_list_entry_get_value(list_entry); } @@ -282,7 +292,7 @@ static char *udev_device_read_symlink(struct udev_device *udev_device, const cha static void udev_device_set_properties_from_uevent(struct udev_device *udev_device) { char line[LINE_MAX], path[PATH_MAX]; - char *key, *val, devnode[PATH_MAX]; + char *pos, devnode[PATH_MAX]; FILE *file; snprintf(path, sizeof(path), "%s/uevent", udev_device_get_syspath(udev_device)); @@ -299,12 +309,9 @@ static void udev_device_set_properties_from_uevent(struct udev_device *udev_devi snprintf(devnode, sizeof(devnode), "/dev/%s", line + 8); udev_list_entry_add(&udev_device->properties, "DEVNAME", devnode, 0); } - else if (strchr(line, '=')) { - val = strchr(line, '=') + 1; - key = line; - key[strcspn(key, "=")] = '\0'; - - udev_list_entry_add(&udev_device->properties, key, val, 1); + else if ((pos = strchr(line, '='))) { + *pos = '\0'; + udev_list_entry_add(&udev_device->properties, line, pos + 1, 1); } } @@ -328,7 +335,7 @@ static int populate_bit(unsigned long *arr, const char *val) bit = strtok_r(bits, " ", &save); - for (i = 0; bit && i < BITS_SIZE; i++) { + for (i = 0; bit && i < BITS_MAX; i++) { arr[i] = strtoul(bit, NULL, 16); bit = strtok_r(NULL, " ", &save); } @@ -357,11 +364,11 @@ static int find_bit(unsigned long *arr, int cnt, int bit) static void udev_device_set_properties_from_evdev(struct udev_device *udev_device) { int ev_cnt, rel_cnt, key_cnt, abs_cnt, prop_cnt; - unsigned long prop_bits[BITS_SIZE] = {0}; - unsigned long abs_bits[BITS_SIZE] = {0}; - unsigned long rel_bits[BITS_SIZE] = {0}; - unsigned long key_bits[BITS_SIZE] = {0}; - unsigned long ev_bits[BITS_SIZE] = {0}; + unsigned long prop_bits[BITS_MAX] = {0}; + unsigned long abs_bits[BITS_MAX] = {0}; + unsigned long rel_bits[BITS_MAX] = {0}; + unsigned long key_bits[BITS_MAX] = {0}; + unsigned long ev_bits[BITS_MAX] = {0}; struct udev_device *parent; const char *subsystem; @@ -371,7 +378,7 @@ static void udev_device_set_properties_from_evdev(struct udev_device *udev_devic return; } - parent = udev_device_get_parent_with_subsystem_devtype(udev_device, "input", NULL); + parent = udev_device; while (1) { if (!parent) { @@ -548,6 +555,72 @@ struct udev_device *udev_device_new_from_subsystem_sysname(struct udev *udev, co return NULL; } +struct udev_device *udev_device_new_from_file(struct udev *udev, const char *path) +{ + char line[LINE_MAX], syspath[PATH_MAX], devnode[PATH_MAX]; + struct udev_device *udev_device; + char *pos, *sysname; + FILE *file; + int i; + + udev_device = calloc(1, sizeof(struct udev_device)); + + if (!udev_device) { + return NULL; + } + + udev_device->udev = udev; + udev_device->refcount = 1; + udev_device->parent = NULL; + + udev_list_entry_init(&udev_device->properties); + udev_list_entry_init(&udev_device->sysattrs); + + file = fopen(path, "r"); + + if (!file) { + return NULL; + } + + while (fgets(line, sizeof(line), file)) { + line[strcspn(line, "\n")] = '\0'; + + if (strncmp(line, "DEVPATH", 7) == 0) { + snprintf(syspath, sizeof(syspath), "/sys%s", line + 8); + udev_list_entry_add(&udev_device->properties, "SYSPATH", syspath, 0); + udev_list_entry_add(&udev_device->properties, "DEVPATH", line + 8, 0); + + sysname = strrchr(syspath, '/') + 1; + udev_list_entry_add(&udev_device->properties, "SYSNAME", sysname, 0); + + for (i = 0; sysname[i] != '\0'; i++) { + if (sysname[i] >= '0' && sysname[i] <= '9') { + udev_list_entry_add(&udev_device->properties, "SYSNUM", sysname + i, 0); + break; + } + } + } + else if (strncmp(line, "DEVNAME", 7) == 0) { + snprintf(devnode, sizeof(devnode), "/dev/%s", line + 8); + udev_list_entry_add(&udev_device->properties, "DEVNAME", devnode, 0); + } + else if ((pos = strchr(line, '='))) { + *pos = '\0'; + udev_list_entry_add(&udev_device->properties, line, pos + 1, 0); + } + } + + fclose(file); + + if (!udev_device_get_syspath(udev_device)) { + udev_device_unref(udev_device); + return NULL; + } + + udev_device_set_properties_from_evdev(udev_device); + return udev_device; +} + struct udev_device *udev_device_new_from_device_id(struct udev *udev, const char *id) { // XXX NOT IMPLEMENTED diff --git a/udev_monitor.c b/udev_monitor.c index 1e88b70..77fac66 100644 --- a/udev_monitor.c +++ b/udev_monitor.c @@ -1,33 +1,180 @@ -#include +#include +#include +#include #include #include +#include +#include +#include +#include +#include +#include #include "udev.h" +#include "udev_list.h" + +#ifndef UDEV_MONITOR_DIR +#define UDEV_MONITOR_DIR "/tmp/.libudev-zero" +#endif + +enum { THREADS_MAX = 5 }; struct udev_monitor { + struct udev_list_entry subsystem_match; + struct udev_list_entry devtype_match; + pthread_t thread[THREADS_MAX]; + pthread_barrier_t barrier; struct udev *udev; int refcount; - int fd[2]; + int sfd[2]; + int ifd; + int efd; }; +static int udev_monitor_filter_devtype(struct udev_monitor *udev_monitor, struct udev_device *udev_device) +{ + struct udev_list_entry *list_entry; + const char *devtype; + + devtype = udev_device_get_devtype(udev_device); + list_entry = udev_list_entry_get_next(&udev_monitor->devtype_match); + + if (!list_entry) { + return 1; + } + + if (!devtype) { + return 0; + } + + while (list_entry) { + if (strcmp(devtype, udev_list_entry_get_name(list_entry)) == 0) { + return 1; + } + + list_entry = udev_list_entry_get_next(list_entry); + } + + return 0; +} + +static int udev_monitor_filter_subsystem(struct udev_monitor *udev_monitor, struct udev_device *udev_device) +{ + struct udev_list_entry *list_entry; + const char *subsystem; + + subsystem = udev_device_get_subsystem(udev_device); + list_entry = udev_list_entry_get_next(&udev_monitor->subsystem_match); + + if (!list_entry) { + return 1; + } + + if (!subsystem) { + return 0; + } + + while (list_entry) { + if (strcmp(subsystem, udev_list_entry_get_name(list_entry)) == 0) { + return 1; + } + + list_entry = udev_list_entry_get_next(list_entry); + } + + return 0; +} + struct udev_device *udev_monitor_receive_device(struct udev_monitor *udev_monitor) { - return (void *)1; + char file[PATH_MAX + sizeof(UDEV_MONITOR_DIR)], data[4096]; + struct udev_device *udev_device; + + if (recv(udev_monitor->sfd[0], data, sizeof(data), 0) == -1) { + return NULL; + } + + snprintf(file, sizeof(file), UDEV_MONITOR_DIR "/%s", data); + udev_device = udev_device_new_from_file(udev_monitor->udev, file); + + if (!udev_device) { + return NULL; + } + + if (!udev_monitor_filter_subsystem(udev_monitor, udev_device) || + !udev_monitor_filter_devtype(udev_monitor, udev_device)) { + udev_device_unref(udev_device); + return NULL; + } + + return udev_device; +} + +static void *udev_monitor_handle_event(void *ptr) +{ + struct udev_monitor *udev_monitor = ptr; + struct inotify_event *event; + struct epoll_event epoll; + char data[4096]; + sigset_t mask; + ssize_t len; + int i; + + sigemptyset(&mask); + + while (epoll_pwait(udev_monitor->efd, &epoll, 1, -1, &mask) != -1) { + len = read(udev_monitor->ifd, data, sizeof(data)); + + if (len == -1) { + continue; + } + + for (i = 0; i < len; i += sizeof(struct inotify_event) + event->len) { + event = (struct inotify_event *)&data[i]; + + // TODO user deleted directory, what should we do ? + if (event->mask & IN_IGNORED) { + break; + } + + send(udev_monitor->sfd[1], event->name, event->len, 0); + } + } + + pthread_barrier_wait(&udev_monitor->barrier); + return NULL; } int udev_monitor_enable_receiving(struct udev_monitor *udev_monitor) { + pthread_attr_t attr; + int i; + + if (!udev_monitor) { + return -1; + } + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + pthread_barrier_init(&udev_monitor->barrier, NULL, THREADS_MAX + 1); + + for (i = 0; i < THREADS_MAX; i++) { + pthread_create(&udev_monitor->thread[i], &attr, udev_monitor_handle_event, udev_monitor); + } + + pthread_attr_destroy(&attr); return 0; } int udev_monitor_set_receive_buffer_size(struct udev_monitor *udev_monitor, int size) { + // XXX NOT IMPLEMENTED return 0; } int udev_monitor_get_fd(struct udev_monitor *udev_monitor) { - return udev_monitor ? udev_monitor->fd[0] : -1; + return udev_monitor ? udev_monitor->sfd[0] : -1; } struct udev *udev_monitor_get_udev(struct udev_monitor *udev_monitor) @@ -37,28 +184,42 @@ struct udev *udev_monitor_get_udev(struct udev_monitor *udev_monitor) int udev_monitor_filter_update(struct udev_monitor *udev_monitor) { + // XXX NOT IMPLEMENTED return 0; } int udev_monitor_filter_remove(struct udev_monitor *udev_monitor) { + // XXX NOT IMPLEMENTED return 0; } int udev_monitor_filter_add_match_subsystem_devtype(struct udev_monitor *udev_monitor, const char *subsystem, const char *devtype) { + if (!udev_monitor || !subsystem) { + return -1; + } + + udev_list_entry_add(&udev_monitor->subsystem_match, subsystem, NULL, 0); + + if (devtype) { + udev_list_entry_add(&udev_monitor->devtype_match, devtype, NULL, 0); + } + return 0; } int udev_monitor_filter_add_match_tag(struct udev_monitor *udev_monitor, const char *tag) { + // XXX NOT IMPLEMENTED return 0; } struct udev_monitor *udev_monitor_new_from_netlink(struct udev *udev, const char *name) { struct udev_monitor *udev_monitor; - int i; + struct epoll_event epoll; + struct stat st; if (!udev || !name) { return NULL; @@ -70,14 +231,54 @@ struct udev_monitor *udev_monitor_new_from_netlink(struct udev *udev, const char return NULL; } - // TODO better no-op, HELP WANTED ! - if (pipe(udev_monitor->fd) == -1) { + if (lstat(UDEV_MONITOR_DIR, &st) != 0) { + if (mkdir(UDEV_MONITOR_DIR, 0) == -1 || + chmod(UDEV_MONITOR_DIR, 0777) == -1) { + free(udev_monitor); + return NULL; + } + } + else if (!S_ISDIR(st.st_mode)) { free(udev_monitor); return NULL; } - for (i = 0; i < 2; i++) { - fcntl(udev_monitor->fd[i], F_SETFD, FD_CLOEXEC | O_NONBLOCK); + udev_monitor->efd = epoll_create1(EPOLL_CLOEXEC); + + if (udev_monitor->efd == -1) { + free(udev_monitor); + return NULL; + } + + udev_monitor->ifd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK); + + if (udev_monitor->ifd == -1) { + close(udev_monitor->efd); + free(udev_monitor); + return NULL; + } + + if (inotify_add_watch(udev_monitor->ifd, UDEV_MONITOR_DIR, IN_CLOSE_WRITE | IN_EXCL_UNLINK) == -1) { + close(udev_monitor->ifd); + close(udev_monitor->efd); + free(udev_monitor); + return NULL; + } + + epoll.events = EPOLLIN | EPOLLET; + + if (epoll_ctl(udev_monitor->efd, EPOLL_CTL_ADD, udev_monitor->ifd, &epoll) == -1) { + close(udev_monitor->ifd); + close(udev_monitor->efd); + free(udev_monitor); + return NULL; + } + + if (socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, udev_monitor->sfd) == -1) { + close(udev_monitor->ifd); + close(udev_monitor->efd); + free(udev_monitor); + return NULL; } udev_monitor->refcount = 1; @@ -107,10 +308,22 @@ struct udev_monitor *udev_monitor_unref(struct udev_monitor *udev_monitor) return NULL; } - for (i = 0; i < 2; i++) { - close(udev_monitor->fd[i]); + udev_list_entry_free_all(&udev_monitor->subsystem_match); + udev_list_entry_free_all(&udev_monitor->devtype_match); + + for (i = 0; i < THREADS_MAX; i++) { + pthread_cancel(udev_monitor->thread[i]); } + pthread_barrier_wait(&udev_monitor->barrier); + pthread_barrier_destroy(&udev_monitor->barrier); + + for (i = 0; i < 2; i++) { + close(udev_monitor->sfd[i]); + } + + close(udev_monitor->ifd); + close(udev_monitor->efd); free(udev_monitor); return NULL; }