mirror of
https://github.com/troglobit/finit.git
synced 2026-10-13 10:14:47 +00:00
keventd: expand scope to become a unified device manager
Evolve keventd from a power_supply-only monitor into a full device
manager capable of replacing mdev/mdevd on embedded systems. This
is the first step towards Finit v5.0 where keventd absorbs devmon.
New capabilities:
- Parse all uevent actions (add, remove, change, bind, unbind)
- Create and remove /dev nodes with subsystem-aware permissions
- Create persistent symlinks in /dev/disk/by-{id,path} and
/dev/input/by-{id,path}, tracked for cleanup on device removal
- Load firmware from /lib/firmware/ via the sysfs loading protocol
- Spawn modprobe for MODALIAS events (async, non-blocking)
- Coldplug support via -c flag (walks /sys/devices to replay events)
- Set dev/* conditions for Finit's service dependency system
The original power_supply monitoring and sys/pwr/ac condition are
preserved.
New files: keventd.h (structures/API), uevent.c (all device logic).
The receive buffer is increased to 8K with a 1MB socket buffer to
reduce event loss during coldplug bursts.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
+2
-2
@@ -124,7 +124,7 @@ AC_PLUGIN([resolvconf], [no], [Setup necessary files for resolvconf])
|
||||
AC_PLUGIN([x11-common], [no], [Console setup (for X)])
|
||||
AC_PLUGIN([netlink], [yes], [Basic netlink plugin for IFUP/IFDN and GW events. Can be replaced with externally built plugin that links with libnl or similar.])
|
||||
AC_PLUGIN([hook-scripts], [no], [Trigger script execution from hook points])
|
||||
AC_PLUGIN([hotplug], [yes], [Start udevd or mdev kernel event datamon])
|
||||
AC_PLUGIN([hotplug], [no], [Start udevd or mdev kernel event datamon])
|
||||
AC_PLUGIN([rtc], [yes], [Save and restore RTC using hwclock])
|
||||
AC_PLUGIN([tty], [yes], [Automatically activate new TTYs, e.g. USB-to-serial])
|
||||
AC_PLUGIN([urandom], [yes], [Setup and save random seed at boot/shutdown])
|
||||
@@ -177,7 +177,7 @@ AC_ARG_WITH(random-seed,
|
||||
[random_seed=$withval], [random_seed=yes])
|
||||
|
||||
AC_ARG_WITH(keventd,
|
||||
AS_HELP_STRING([--with-keventd], [Enable built-in keventd, default: no]),, [with_keventd=no])
|
||||
AS_HELP_STRING([--with-keventd], [Enable built-in keventd device manager, default: yes]),, [with_keventd=yes])
|
||||
|
||||
AC_ARG_WITH(sulogin,
|
||||
AS_HELP_STRING([--with-sulogin@<:@=USER@:>@], [Enable built-in sulogin, optional USER to request password for (default root), default: no.]),[sulogin=$withval],[with_sulogin=no])
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ getty_CFLAGS = -W -Wall -Wextra -std=gnu99
|
||||
getty_CFLAGS += $(lite_CFLAGS)
|
||||
getty_LDADD = $(lite_LIBS)
|
||||
|
||||
keventd_SOURCES = keventd.c iwatch.c iwatch.h util.c util.h
|
||||
keventd_SOURCES = keventd.c keventd.h uevent.c util.c util.h
|
||||
keventd_CFLAGS = -W -Wall -Wextra -std=gnu99
|
||||
keventd_CFLAGS += $(lite_CFLAGS)
|
||||
keventd_LDADD = $(lite_LIBS)
|
||||
|
||||
+216
-59
@@ -1,4 +1,17 @@
|
||||
/* Listens to kernel events like AC power status and manages sys/ conditions
|
||||
/* Unified device manager - kernel events, device nodes, symlinks, conditions
|
||||
*
|
||||
* TODO / known limitations
|
||||
* - firmware_load() copies synchronously in the main event loop; a
|
||||
* multi-MB blob (e.g. iwlwifi) blocks all other event handling for
|
||||
* the duration. Worker pool / fork-per-event would unblock this
|
||||
* (see .notes/keventd-eudev-gap-analysis.md, gap #8).
|
||||
* - firmware_load() does not handle compressed firmware (.xz, .zst).
|
||||
* Modern linux-firmware ships .xz blobs; kernels with
|
||||
* CONFIG_FW_LOADER_COMPRESS decompress in-kernel via direct
|
||||
* loading, but the legacy /sys/.../loading path used here passes
|
||||
* raw bytes only. Adding liblzma/libzstd would close this gap for
|
||||
* drivers using the userhelper fallback on systems without
|
||||
* CONFIG_FW_LOADER_COMPRESS.
|
||||
*
|
||||
* Copyright (c) 2021-2025 Joachim Wiberg <troglobit@gmail.com>
|
||||
*
|
||||
@@ -23,6 +36,7 @@
|
||||
|
||||
#include <errno.h>
|
||||
#include <dirent.h>
|
||||
#include <getopt.h>
|
||||
#include <poll.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
@@ -33,6 +47,7 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <linux/types.h>
|
||||
@@ -44,11 +59,13 @@
|
||||
# include <lite/lite.h>
|
||||
#endif
|
||||
|
||||
#include "keventd.h"
|
||||
#include "cond.h"
|
||||
#include "pid.h"
|
||||
#include "util.h"
|
||||
|
||||
#define _PATH_SYSFS_PWR "/sys/class/power_supply"
|
||||
#define KEVENTD_VERSION "5.0"
|
||||
#define _PATH_SYSFS_PWR "/sys/class/power_supply"
|
||||
|
||||
static int num_ac_online;
|
||||
static int num_ac;
|
||||
@@ -75,7 +92,7 @@ void logit(int prio, const char *fmt, ...)
|
||||
#define panic(fmt, args...) { logit(LOG_CRIT, fmt ":%s", ##args, strerror(errno)); exit(1); }
|
||||
#define warn(fmt, args...) { logit(LOG_WARNING, fmt ":%s", ##args, strerror(errno)); }
|
||||
|
||||
static void sys_cond(char *cond, int set)
|
||||
static void sys_cond(const char *cond, int set)
|
||||
{
|
||||
char oneshot[256];
|
||||
|
||||
@@ -108,7 +125,7 @@ static int fgetline(char *path, char *buf, size_t len)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int check_online(char *online)
|
||||
static int check_online(const char *online)
|
||||
{
|
||||
int val;
|
||||
|
||||
@@ -121,9 +138,9 @@ static int check_online(char *online)
|
||||
return val;
|
||||
}
|
||||
|
||||
static int is_ac(char *type)
|
||||
static int is_ac(const char *type)
|
||||
{
|
||||
char *types[] = {
|
||||
static const char *types[] = {
|
||||
"Mains",
|
||||
"USB",
|
||||
"BrickID",
|
||||
@@ -140,7 +157,103 @@ static int is_ac(char *type)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void init(void)
|
||||
/*
|
||||
* Handle power_supply change events (original keventd functionality).
|
||||
*/
|
||||
static void power_supply_change(struct uevent *ev, char *buf, size_t len)
|
||||
{
|
||||
int ac = 0;
|
||||
size_t i, hdrlen;
|
||||
|
||||
/* Skip past header to key=value pairs */
|
||||
hdrlen = strlen(buf) + 1;
|
||||
if (ev->devpath)
|
||||
hdrlen += strlen(ev->devpath) + 1;
|
||||
|
||||
for (i = hdrlen; i < len; ) {
|
||||
char *line = buf + i;
|
||||
|
||||
if (!*line)
|
||||
break;
|
||||
|
||||
if (!strncmp(line, "POWER_SUPPLY_TYPE=", 18)) {
|
||||
ac = is_ac(&line[18]);
|
||||
} else if (!strncmp(line, "POWER_SUPPLY_ONLINE=", 20) && ac) {
|
||||
if (check_online(&line[20])) {
|
||||
if (!num_ac_online)
|
||||
sys_cond("pwr/ac", 1);
|
||||
num_ac_online++;
|
||||
} else {
|
||||
if (num_ac_online > 0)
|
||||
num_ac_online--;
|
||||
if (!num_ac_online)
|
||||
sys_cond("pwr/ac", 0);
|
||||
}
|
||||
}
|
||||
|
||||
i += strlen(line) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Handle a single uevent from the kernel.
|
||||
*/
|
||||
static void handle_uevent(char *buf, size_t len)
|
||||
{
|
||||
struct uevent ev;
|
||||
|
||||
if (uevent_parse(buf, len, &ev))
|
||||
return;
|
||||
|
||||
logit(LOG_DEBUG, "uevent: %s@%s subsys=%s dev=%s major=%d minor=%d",
|
||||
uevent_action_str(ev.action), ev.devpath ?: "",
|
||||
ev.subsystem ?: "", ev.devname ?: "",
|
||||
ev.major, ev.minor);
|
||||
|
||||
switch (ev.action) {
|
||||
case ACT_ADD:
|
||||
/* Firmware loading takes priority */
|
||||
if (ev.firmware)
|
||||
firmware_load(&ev);
|
||||
|
||||
/* Module loading */
|
||||
if (ev.modalias)
|
||||
modprobe_load(ev.modalias);
|
||||
|
||||
/* Create device node if we have the info */
|
||||
if (ev.major >= 0 && ev.minor >= 0 && ev.devname)
|
||||
devnode_add(&ev);
|
||||
|
||||
/* Create symlinks */
|
||||
symlink_add(&ev);
|
||||
break;
|
||||
|
||||
case ACT_REMOVE:
|
||||
/* Remove symlinks first */
|
||||
symlink_del(&ev);
|
||||
|
||||
/* Remove device node */
|
||||
if (ev.devname)
|
||||
devnode_del(&ev);
|
||||
break;
|
||||
|
||||
case ACT_CHANGE:
|
||||
/* Handle power supply changes */
|
||||
if (ev.subsystem && !strcmp(ev.subsystem, "power_supply"))
|
||||
power_supply_change(&ev, buf, len);
|
||||
break;
|
||||
|
||||
case ACT_BIND:
|
||||
case ACT_UNBIND:
|
||||
/* Driver bind/unbind - could trigger conditions */
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void init_power_supply(void)
|
||||
{
|
||||
struct dirent **d = NULL;
|
||||
char *cond_dirs[] = {
|
||||
@@ -161,7 +274,7 @@ static void init(void)
|
||||
n = scandir(_PATH_SYSFS_PWR, &d, NULL, alphasort);
|
||||
for (i = 0; i < n; i++) {
|
||||
char *nm = d[i]->d_name;
|
||||
char buf[10];
|
||||
char buf[10];
|
||||
|
||||
snprintf(path, sizeof(path), "%s/%s/type", _PATH_SYSFS_PWR, nm);
|
||||
if (!fgetline(path, buf, sizeof(buf)) && is_ac(buf)) {
|
||||
@@ -184,6 +297,16 @@ static void init(void)
|
||||
sys_cond("pwr/ac", 1);
|
||||
}
|
||||
|
||||
static void init_dev_condition_dir(void)
|
||||
{
|
||||
char dir[256];
|
||||
|
||||
/* Create /run/finit/cond/dev/ directory for device conditions */
|
||||
snprintf(dir, sizeof(dir), "%s", _PATH_CONDDEV);
|
||||
if (mkpath(dir, 0755) && errno != EEXIST)
|
||||
warn("Failed creating dev condition directory %s", dir);
|
||||
}
|
||||
|
||||
static void set_logging(int prio)
|
||||
{
|
||||
setlogmask(LOG_UPTO(prio));
|
||||
@@ -204,109 +327,143 @@ static void shut_down(int signo)
|
||||
running = 0;
|
||||
}
|
||||
|
||||
static int usage(int rc)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"Usage: keventd [-dhnv] [-c]\n"
|
||||
"\n"
|
||||
"Options:\n"
|
||||
" -c Run coldplug at startup\n"
|
||||
" -d Enable debug mode (foreground, verbose)\n"
|
||||
" -h Show this help text\n"
|
||||
" -n Run in foreground (no daemon)\n"
|
||||
" -v Show version\n"
|
||||
"\n");
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
* Unified device manager daemon.
|
||||
*
|
||||
* Started by Finit as soon as possible when base filesystem is up,
|
||||
* modules have been probed, or insmodded from /etc/finit.conf, so by
|
||||
* now we should have /sys/class/power_supply/ available for probing.
|
||||
* If none is found we assert /sys/pwr/ac condition anyway, this is what
|
||||
* systemd does (ConditionACPower) and also makes most sense.
|
||||
* modules have been probed. Handles:
|
||||
* - Device node creation/removal in /dev
|
||||
* - Persistent symlinks in /dev/disk/by-*, /dev/input/by-*
|
||||
* - Module loading via MODALIAS
|
||||
* - Firmware loading via FIRMWARE
|
||||
* - Power supply conditions (sys/pwr/ac)
|
||||
* - Device conditions (dev/)
|
||||
*/
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
struct sockaddr_nl nls = { 0 };
|
||||
struct pollfd pfd;
|
||||
char buf[1024];
|
||||
char buf[UEVENT_BUFFER_SIZE];
|
||||
int do_coldplug = 0;
|
||||
int foreground = 0;
|
||||
int c;
|
||||
|
||||
if (argc > 1) {
|
||||
if (!strcmp(argv[1], "-d"))
|
||||
/* Device nodes are created with explicit MODE= from rules or the
|
||||
* built-in devrules table; clear umask so mknod() honors the
|
||||
* requested bits verbatim instead of masking them. */
|
||||
umask(0);
|
||||
|
||||
while ((c = getopt(argc, argv, "cdhnv")) != -1) {
|
||||
switch (c) {
|
||||
case 'c':
|
||||
do_coldplug = 1;
|
||||
break;
|
||||
case 'd':
|
||||
debug = 1;
|
||||
foreground = 1;
|
||||
break;
|
||||
case 'h':
|
||||
return usage(0);
|
||||
case 'n':
|
||||
foreground = 1;
|
||||
break;
|
||||
case 'v':
|
||||
printf("keventd v%s\n", KEVENTD_VERSION);
|
||||
return 0;
|
||||
default:
|
||||
return usage(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!debug) {
|
||||
if (!foreground) {
|
||||
openlog("keventd", LOG_PID, LOG_DAEMON);
|
||||
set_logging(LOG_NOTICE);
|
||||
logon = 1;
|
||||
} else {
|
||||
set_logging(debug ? LOG_DEBUG : LOG_NOTICE);
|
||||
}
|
||||
|
||||
signal(SIGUSR1, toggle_debug);
|
||||
signal(SIGTERM, shut_down);
|
||||
init();
|
||||
signal(SIGCHLD, SIG_IGN); /* Don't wait for modprobe children */
|
||||
|
||||
/* Initialize condition directories */
|
||||
init_power_supply();
|
||||
init_dev_condition_dir();
|
||||
|
||||
/* Set up netlink socket for kernel uevents */
|
||||
pfd.events = POLLIN;
|
||||
pfd.fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
|
||||
pfd.fd = socket(PF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT);
|
||||
if (pfd.fd == -1)
|
||||
panic("failed creating netlink socket");
|
||||
|
||||
nls.nl_family = AF_NETLINK;
|
||||
nls.nl_pid = 0;
|
||||
nls.nl_groups = -1;
|
||||
nls.nl_groups = 1; /* Kernel uevents are on group 1 only */
|
||||
if (bind(pfd.fd, (void *)&nls, sizeof(struct sockaddr_nl)))
|
||||
panic("bind failed");
|
||||
|
||||
logit(LOG_DEBUG, "Waiting for events ...");
|
||||
/* Increase receive buffer to reduce event loss */
|
||||
{
|
||||
int rcvbuf = 1024 * 1024;
|
||||
setsockopt(pfd.fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
|
||||
}
|
||||
|
||||
/* Run coldplug if requested */
|
||||
if (do_coldplug)
|
||||
coldplug();
|
||||
|
||||
logit(LOG_NOTICE, "keventd v%s started, waiting for events...", KEVENTD_VERSION);
|
||||
|
||||
while (running) {
|
||||
char *path = NULL;
|
||||
int ac = 0;
|
||||
int i, len;
|
||||
int len;
|
||||
|
||||
if (-1 == poll(&pfd, 1, -1)) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
len = recv(pfd.fd, buf, sizeof(buf), MSG_DONTWAIT);
|
||||
len = recv(pfd.fd, buf, sizeof(buf) - 1, MSG_DONTWAIT);
|
||||
if (len == -1) {
|
||||
switch (errno) {
|
||||
case EINTR:
|
||||
continue;
|
||||
case ENOBUFS:
|
||||
warn("lost events");
|
||||
warn("lost events, buffer overflow");
|
||||
continue;
|
||||
default:
|
||||
panic("unhandled");
|
||||
panic("recv failed");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
buf[len] = 0;
|
||||
logit(LOG_DEBUG, "%s", buf);
|
||||
|
||||
/* skip libusb events, focus on kernel events/changes */
|
||||
if (strncmp(buf, "change@", 7))
|
||||
/* Skip libudev events (start with "libudev") */
|
||||
if (!strncmp(buf, "libudev", 7))
|
||||
continue;
|
||||
|
||||
/* XXX: currently limited to monitoring this subsystem */
|
||||
if (!strstr(buf, "power_supply"))
|
||||
continue;
|
||||
|
||||
i = 0;
|
||||
while (i < len) {
|
||||
char *line = buf + i;
|
||||
|
||||
logit(LOG_DEBUG, "%s", line);
|
||||
if (!strncmp(line, "DEVPATH=", 8)) {
|
||||
path = &line[8];
|
||||
logit(LOG_DEBUG, "Got path %s", path);
|
||||
} else if (!strncmp(line, "POWER_SUPPLY_TYPE=", 18)) {
|
||||
ac = is_ac(&line[18]);
|
||||
} else if (!strncmp(line, "POWER_SUPPLY_ONLINE=", 20) && ac) {
|
||||
if (check_online(&line[20])) {
|
||||
if (!num_ac_online)
|
||||
sys_cond("pwr/ac", 1);
|
||||
num_ac_online++;
|
||||
} else {
|
||||
if (num_ac_online > 0)
|
||||
num_ac_online--;
|
||||
if (!num_ac_online)
|
||||
sys_cond("pwr/ac", 0);
|
||||
}
|
||||
}
|
||||
|
||||
i += strlen(line) + 1;
|
||||
}
|
||||
handle_uevent(buf, len);
|
||||
}
|
||||
|
||||
close(pfd.fd);
|
||||
logit(LOG_NOTICE, "keventd shutting down");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/* Unified device manager for Finit - uevent handling, device nodes, symlinks
|
||||
*
|
||||
* Copyright (c) 2021-2025 Joachim Wiberg <troglobit@gmail.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef FINIT_KEVENTD_H_
|
||||
#define FINIT_KEVENTD_H_
|
||||
|
||||
#ifdef _LIBITE_LITE
|
||||
# include <libite/queue.h> /* BSD sys/queue.h API */
|
||||
#else
|
||||
# include <lite/queue.h> /* BSD sys/queue.h API */
|
||||
#endif
|
||||
#include <sys/types.h>
|
||||
|
||||
/* Maximum uevent buffer size (kernel uses 8192 internally) */
|
||||
#define UEVENT_BUFFER_SIZE 8192
|
||||
|
||||
/* Uevent actions from kernel */
|
||||
typedef enum {
|
||||
ACT_UNKNOWN = 0,
|
||||
ACT_ADD,
|
||||
ACT_REMOVE,
|
||||
ACT_CHANGE,
|
||||
ACT_MOVE,
|
||||
ACT_ONLINE,
|
||||
ACT_OFFLINE,
|
||||
ACT_BIND,
|
||||
ACT_UNBIND,
|
||||
} uevent_action_t;
|
||||
|
||||
/*
|
||||
* Parsed uevent structure.
|
||||
* Pointers are into the receive buffer, zero-copy.
|
||||
*/
|
||||
struct uevent {
|
||||
uevent_action_t action;
|
||||
char *devpath; /* /devices/pci0000:00/... */
|
||||
char *subsystem; /* block, input, net, power_supply */
|
||||
char *devname; /* sda, event0, ttyUSB0 */
|
||||
char *devtype; /* disk, partition */
|
||||
int major;
|
||||
int minor;
|
||||
char *modalias; /* module alias for auto-loading */
|
||||
char *firmware; /* firmware file name request */
|
||||
char *seqnum; /* kernel sequence number */
|
||||
char *driver; /* driver name */
|
||||
};
|
||||
|
||||
/* Tracked symlink for cleanup on device removal */
|
||||
struct dev_symlink {
|
||||
TAILQ_ENTRY(dev_symlink) link;
|
||||
char *devpath; /* sysfs devpath (key for removal) */
|
||||
char *linkpath; /* /dev/disk/by-id/... */
|
||||
};
|
||||
|
||||
/* Function prototypes - uevent.c */
|
||||
int uevent_parse (char *buf, size_t len, struct uevent *ev);
|
||||
const char *uevent_action_str(uevent_action_t action);
|
||||
|
||||
int devnode_add (struct uevent *ev);
|
||||
int devnode_del (struct uevent *ev);
|
||||
|
||||
int symlink_add (struct uevent *ev);
|
||||
int symlink_del (struct uevent *ev);
|
||||
|
||||
int firmware_load (struct uevent *ev);
|
||||
int modprobe_load (const char *modalias);
|
||||
|
||||
int coldplug (void);
|
||||
|
||||
#endif /* FINIT_KEVENTD_H_ */
|
||||
|
||||
/**
|
||||
* Local Variables:
|
||||
* indent-tabs-mode: t
|
||||
* c-file-style: "linux"
|
||||
* End:
|
||||
*/
|
||||
+898
@@ -0,0 +1,898 @@
|
||||
/* Uevent parsing, device node management, symlinks, and firmware loading
|
||||
*
|
||||
* Copyright (c) 2021-2025 Joachim Wiberg <troglobit@gmail.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <fnmatch.h>
|
||||
#include <ftw.h>
|
||||
#include <libgen.h>
|
||||
#include <limits.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <syslog.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <sys/sysmacros.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/utsname.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#ifdef _LIBITE_LITE
|
||||
# include <libite/lite.h>
|
||||
#else
|
||||
# include <lite/lite.h>
|
||||
#endif
|
||||
|
||||
#include "keventd.h"
|
||||
#include "cond.h"
|
||||
#include "util.h"
|
||||
|
||||
/* Forward declarations */
|
||||
void logit(int prio, const char *fmt, ...);
|
||||
|
||||
/*
|
||||
* Set/clear a device condition by creating/removing a symlink.
|
||||
* keventd is a standalone daemon, so we manipulate the filesystem directly
|
||||
* rather than using Finit's internal cond_set()/cond_clear() API.
|
||||
*/
|
||||
static void dev_cond(const char *devname, int set)
|
||||
{
|
||||
char cond[PATH_MAX];
|
||||
char *dir;
|
||||
|
||||
if (!devname)
|
||||
return;
|
||||
|
||||
snprintf(cond, sizeof(cond), "%s%s", _PATH_CONDDEV, devname);
|
||||
|
||||
/* Create parent directory if needed (e.g., dev/input/) */
|
||||
dir = strdupa(cond);
|
||||
dir = dirname(dir);
|
||||
if (strcmp(dir, _PATH_CONDDEV)) {
|
||||
if (mkpath(dir, 0755) && errno != EEXIST)
|
||||
logit(LOG_WARNING, "Failed creating condition dir %s", dir);
|
||||
}
|
||||
|
||||
if (set) {
|
||||
if (symlink(_PATH_RECONF, cond) && errno != EEXIST)
|
||||
logit(LOG_WARNING, "Failed setting dev/%s condition", devname);
|
||||
} else {
|
||||
if (erase(cond) && errno != ENOENT)
|
||||
logit(LOG_WARNING, "Failed clearing dev/%s condition", devname);
|
||||
}
|
||||
}
|
||||
|
||||
/* Symlink tracking for cleanup on device removal */
|
||||
static TAILQ_HEAD(, dev_symlink) symlinks = TAILQ_HEAD_INITIALIZER(symlinks);
|
||||
|
||||
/*
|
||||
* Default device permissions based on subsystem/name.
|
||||
* Simple built-in rules, no config file needed.
|
||||
*/
|
||||
struct devrule {
|
||||
const char *subsystem; /* NULL matches any */
|
||||
const char *pattern; /* fnmatch pattern, NULL = default */
|
||||
mode_t mode;
|
||||
uid_t uid;
|
||||
gid_t gid;
|
||||
};
|
||||
|
||||
static struct devrule devrules[] = {
|
||||
/* Block devices */
|
||||
{ "block", "sd[a-z]*", 0660, 0, 6 }, /* root:disk */
|
||||
{ "block", "vd[a-z]*", 0660, 0, 6 },
|
||||
{ "block", "nvme*", 0660, 0, 6 },
|
||||
{ "block", "mmcblk*", 0660, 0, 6 },
|
||||
{ "block", "loop*", 0660, 0, 6 },
|
||||
{ "block", "dm-*", 0660, 0, 6 },
|
||||
{ "block", "md*", 0660, 0, 6 },
|
||||
{ "block", NULL, 0660, 0, 6 }, /* default block */
|
||||
|
||||
/* TTY devices */
|
||||
{ "tty", "tty[0-9]*", 0620, 0, 5 }, /* root:tty */
|
||||
{ "tty", "ttyS*", 0660, 0, 20 }, /* root:dialout */
|
||||
{ "tty", "ttyUSB*", 0660, 0, 20 },
|
||||
{ "tty", "ttyACM*", 0660, 0, 20 },
|
||||
{ "tty", NULL, 0666, 0, 5 },
|
||||
|
||||
/* Input devices */
|
||||
{ "input", "event*", 0660, 0, 0 }, /* root:input (13) */
|
||||
{ "input", "mouse*", 0660, 0, 0 },
|
||||
{ "input", "mice", 0660, 0, 0 },
|
||||
{ "input", NULL, 0660, 0, 0 },
|
||||
|
||||
/* Sound devices */
|
||||
{ "sound", NULL, 0660, 0, 29 }, /* root:audio */
|
||||
|
||||
/* Video devices */
|
||||
{ "video4linux", NULL, 0660, 0, 44 }, /* root:video */
|
||||
|
||||
/* DRM (graphics) */
|
||||
{ "drm", "card*", 0660, 0, 44 },
|
||||
{ "drm", "render*", 0660, 0, 44 },
|
||||
|
||||
/* USB devices */
|
||||
{ "usb", NULL, 0664, 0, 0 },
|
||||
|
||||
/* Network devices - no /dev node needed */
|
||||
|
||||
/* Common char devices - match by name regardless of subsystem */
|
||||
{ NULL, "null", 0666, 0, 0 },
|
||||
{ NULL, "zero", 0666, 0, 0 },
|
||||
{ NULL, "full", 0666, 0, 0 },
|
||||
{ NULL, "random", 0666, 0, 0 },
|
||||
{ NULL, "urandom", 0666, 0, 0 },
|
||||
{ NULL, "tty", 0666, 0, 5 },
|
||||
{ NULL, "console", 0600, 0, 0 },
|
||||
{ NULL, "ptmx", 0666, 0, 5 },
|
||||
{ NULL, "kmsg", 0640, 0, 0 },
|
||||
{ NULL, "mem", 0640, 0, 0 }, /* root:kmem */
|
||||
{ NULL, "kmem", 0640, 0, 0 },
|
||||
{ NULL, "port", 0640, 0, 0 },
|
||||
{ NULL, "fuse", 0666, 0, 0 },
|
||||
{ NULL, "kvm", 0660, 0, 0 },
|
||||
|
||||
/* Default fallback */
|
||||
{ NULL, NULL, 0660, 0, 0 },
|
||||
};
|
||||
|
||||
static uevent_action_t parse_action(const char *str)
|
||||
{
|
||||
if (!strcmp(str, "add"))
|
||||
return ACT_ADD;
|
||||
if (!strcmp(str, "remove"))
|
||||
return ACT_REMOVE;
|
||||
if (!strcmp(str, "change"))
|
||||
return ACT_CHANGE;
|
||||
if (!strcmp(str, "move"))
|
||||
return ACT_MOVE;
|
||||
if (!strcmp(str, "online"))
|
||||
return ACT_ONLINE;
|
||||
if (!strcmp(str, "offline"))
|
||||
return ACT_OFFLINE;
|
||||
if (!strcmp(str, "bind"))
|
||||
return ACT_BIND;
|
||||
if (!strcmp(str, "unbind"))
|
||||
return ACT_UNBIND;
|
||||
|
||||
return ACT_UNKNOWN;
|
||||
}
|
||||
|
||||
const char *uevent_action_str(uevent_action_t action)
|
||||
{
|
||||
switch (action) {
|
||||
case ACT_ADD: return "add";
|
||||
case ACT_REMOVE: return "remove";
|
||||
case ACT_CHANGE: return "change";
|
||||
case ACT_MOVE: return "move";
|
||||
case ACT_ONLINE: return "online";
|
||||
case ACT_OFFLINE: return "offline";
|
||||
case ACT_BIND: return "bind";
|
||||
case ACT_UNBIND: return "unbind";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse a uevent message from kernel netlink socket.
|
||||
*
|
||||
* Format:
|
||||
* ACTION@DEVPATH\0
|
||||
* KEY=VALUE\0
|
||||
* KEY=VALUE\0
|
||||
* ...
|
||||
* \0
|
||||
*/
|
||||
int uevent_parse(char *buf, size_t len, struct uevent *ev)
|
||||
{
|
||||
char *at, *line;
|
||||
size_t i, hdrlen;
|
||||
|
||||
memset(ev, 0, sizeof(*ev));
|
||||
ev->major = -1;
|
||||
ev->minor = -1;
|
||||
|
||||
/* Find ACTION@DEVPATH separator */
|
||||
at = strchr(buf, '@');
|
||||
if (!at)
|
||||
return -1;
|
||||
|
||||
/* Split action and devpath */
|
||||
*at = 0;
|
||||
ev->action = parse_action(buf);
|
||||
ev->devpath = at + 1;
|
||||
|
||||
/* Skip past the header to the key=value pairs */
|
||||
hdrlen = strlen(buf) + 1 + strlen(ev->devpath) + 1;
|
||||
i = hdrlen;
|
||||
|
||||
/* Parse KEY=VALUE pairs */
|
||||
while (i < len) {
|
||||
char *eq;
|
||||
|
||||
line = buf + i;
|
||||
if (!*line)
|
||||
break;
|
||||
|
||||
eq = strchr(line, '=');
|
||||
if (eq) {
|
||||
*eq = 0;
|
||||
eq++;
|
||||
|
||||
if (!strcmp(line, "SUBSYSTEM"))
|
||||
ev->subsystem = eq;
|
||||
else if (!strcmp(line, "DEVNAME"))
|
||||
ev->devname = eq;
|
||||
else if (!strcmp(line, "INTERFACE") && !ev->devname)
|
||||
ev->devname = eq; /* net devices use INTERFACE=, not DEVNAME= */
|
||||
else if (!strcmp(line, "DEVTYPE"))
|
||||
ev->devtype = eq;
|
||||
else if (!strcmp(line, "MAJOR"))
|
||||
ev->major = atoi(eq);
|
||||
else if (!strcmp(line, "MINOR"))
|
||||
ev->minor = atoi(eq);
|
||||
else if (!strcmp(line, "MODALIAS"))
|
||||
ev->modalias = eq;
|
||||
else if (!strcmp(line, "FIRMWARE"))
|
||||
ev->firmware = eq;
|
||||
else if (!strcmp(line, "SEQNUM"))
|
||||
ev->seqnum = eq;
|
||||
else if (!strcmp(line, "DRIVER"))
|
||||
ev->driver = eq;
|
||||
}
|
||||
|
||||
i += strlen(line) + (eq ? strlen(eq) + 1 : 1) + 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static struct devrule *find_rule(struct uevent *ev)
|
||||
{
|
||||
struct devrule *rule;
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < NELEMS(devrules); i++) {
|
||||
rule = &devrules[i];
|
||||
|
||||
/* Check subsystem if rule specifies one */
|
||||
if (rule->subsystem && ev->subsystem) {
|
||||
if (strcmp(rule->subsystem, ev->subsystem))
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check pattern if rule specifies one */
|
||||
if (rule->pattern && ev->devname) {
|
||||
/* Just match basename, not full path */
|
||||
const char *name = strrchr(ev->devname, '/');
|
||||
name = name ? name + 1 : ev->devname;
|
||||
|
||||
if (fnmatch(rule->pattern, name, 0))
|
||||
continue;
|
||||
}
|
||||
|
||||
return rule;
|
||||
}
|
||||
|
||||
/* Return last rule as default */
|
||||
return &devrules[NELEMS(devrules) - 1];
|
||||
}
|
||||
|
||||
/*
|
||||
* Create device node in /dev.
|
||||
*/
|
||||
int devnode_add(struct uevent *ev)
|
||||
{
|
||||
struct devrule *rule;
|
||||
char path[PATH_MAX];
|
||||
char *dir;
|
||||
mode_t mode;
|
||||
dev_t dev;
|
||||
int rc;
|
||||
|
||||
if (!ev->devname || ev->major < 0 || ev->minor < 0)
|
||||
return -1;
|
||||
|
||||
snprintf(path, sizeof(path), "/dev/%s", ev->devname);
|
||||
|
||||
/* Create parent directories if needed (e.g., /dev/input/) */
|
||||
dir = strdupa(path);
|
||||
dir = dirname(dir);
|
||||
if (strcmp(dir, "/dev")) {
|
||||
rc = mkpath(dir, 0755);
|
||||
if (rc && errno != EEXIST) {
|
||||
logit(LOG_ERR, "Failed creating %s: %s", dir, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Find matching rule for permissions */
|
||||
rule = find_rule(ev);
|
||||
mode = rule->mode;
|
||||
dev = makedev(ev->major, ev->minor);
|
||||
|
||||
/* Remove existing node if present */
|
||||
unlink(path);
|
||||
|
||||
/* Create device node */
|
||||
if (ev->subsystem && !strcmp(ev->subsystem, "block"))
|
||||
mode |= S_IFBLK;
|
||||
else
|
||||
mode |= S_IFCHR;
|
||||
|
||||
rc = mknod(path, mode, dev);
|
||||
if (rc) {
|
||||
logit(LOG_ERR, "Failed creating %s: %s", path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* mknod() applies umask; chmod() does not. Explicitly chmod to
|
||||
* the requested mode so the result is independent of umask state. */
|
||||
if (chmod(path, mode & ~S_IFMT))
|
||||
logit(LOG_WARNING, "Failed chmod %s: %s", path, strerror(errno));
|
||||
|
||||
/* Set ownership */
|
||||
if (chown(path, rule->uid, rule->gid))
|
||||
logit(LOG_WARNING, "Failed chown %s: %s", path, strerror(errno));
|
||||
|
||||
logit(LOG_DEBUG, "Created %s (%d:%d) mode %04o",
|
||||
path, ev->major, ev->minor, rule->mode);
|
||||
|
||||
/* Set dev/ condition */
|
||||
dev_cond(ev->devname, 1);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove device node from /dev.
|
||||
*/
|
||||
int devnode_del(struct uevent *ev)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
|
||||
if (!ev->devname)
|
||||
return -1;
|
||||
|
||||
snprintf(path, sizeof(path), "/dev/%s", ev->devname);
|
||||
|
||||
/* Clear dev/ condition first */
|
||||
dev_cond(ev->devname, 0);
|
||||
|
||||
if (unlink(path) && errno != ENOENT) {
|
||||
logit(LOG_WARNING, "Failed removing %s: %s", path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
logit(LOG_DEBUG, "Removed %s", path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Track symlink for removal when device is unplugged.
|
||||
*/
|
||||
static void symlink_track(const char *devpath, const char *linkpath)
|
||||
{
|
||||
struct dev_symlink *sl;
|
||||
|
||||
sl = malloc(sizeof(*sl));
|
||||
if (!sl)
|
||||
return;
|
||||
|
||||
sl->devpath = strdup(devpath);
|
||||
sl->linkpath = strdup(linkpath);
|
||||
|
||||
if (!sl->devpath || !sl->linkpath) {
|
||||
free(sl->devpath);
|
||||
free(sl->linkpath);
|
||||
free(sl);
|
||||
return;
|
||||
}
|
||||
|
||||
TAILQ_INSERT_TAIL(&symlinks, sl, link);
|
||||
}
|
||||
|
||||
/*
|
||||
* Create a symlink and track it.
|
||||
*/
|
||||
static int symlink_create(const char *target, const char *link, const char *devpath)
|
||||
{
|
||||
char *dir;
|
||||
int rc;
|
||||
|
||||
/* Create parent directories */
|
||||
dir = strdupa(link);
|
||||
dir = dirname(dir);
|
||||
rc = mkpath(dir, 0755);
|
||||
if (rc && errno != EEXIST)
|
||||
return -1;
|
||||
|
||||
/* Remove existing link */
|
||||
unlink(link);
|
||||
|
||||
/* Create symlink */
|
||||
if (symlink(target, link)) {
|
||||
if (errno != EEXIST)
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Track for removal */
|
||||
symlink_track(devpath, link);
|
||||
|
||||
logit(LOG_DEBUG, "Created symlink %s -> %s", link, target);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Read a single line from a sysfs attribute file.
|
||||
*/
|
||||
static int sysfs_read(const char *path, char *buf, size_t len)
|
||||
{
|
||||
FILE *fp;
|
||||
|
||||
fp = fopen(path, "r");
|
||||
if (!fp)
|
||||
return -1;
|
||||
|
||||
if (!fgets(buf, len, fp)) {
|
||||
fclose(fp);
|
||||
return -1;
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
chomp(buf);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Build disk ID string from sysfs attributes.
|
||||
* Format: BUSTYPE-VENDOR_MODEL_SERIAL
|
||||
*/
|
||||
static int disk_serial_id(struct uevent *ev, char *id, size_t len)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
char vendor[64] = "", model[64] = "", serial[64] = "";
|
||||
char *p;
|
||||
|
||||
/* Try to read from device's sysfs attributes */
|
||||
snprintf(path, sizeof(path), "/sys%s/device/vendor", ev->devpath);
|
||||
sysfs_read(path, vendor, sizeof(vendor));
|
||||
|
||||
snprintf(path, sizeof(path), "/sys%s/device/model", ev->devpath);
|
||||
sysfs_read(path, model, sizeof(model));
|
||||
|
||||
snprintf(path, sizeof(path), "/sys%s/device/serial", ev->devpath);
|
||||
if (sysfs_read(path, serial, sizeof(serial))) {
|
||||
/* Try alternate location */
|
||||
snprintf(path, sizeof(path), "/sys%s/../serial", ev->devpath);
|
||||
sysfs_read(path, serial, sizeof(serial));
|
||||
}
|
||||
|
||||
/* Need at least model or serial */
|
||||
if (!model[0] && !serial[0])
|
||||
return -1;
|
||||
|
||||
/* Clean up strings - replace spaces with underscores */
|
||||
for (p = vendor; *p; p++)
|
||||
if (*p == ' ') *p = '_';
|
||||
for (p = model; *p; p++)
|
||||
if (*p == ' ') *p = '_';
|
||||
for (p = serial; *p; p++)
|
||||
if (*p == ' ') *p = '_';
|
||||
|
||||
/* Remove trailing underscores */
|
||||
for (p = vendor + strlen(vendor) - 1; p >= vendor && *p == '_'; p--)
|
||||
*p = 0;
|
||||
for (p = model + strlen(model) - 1; p >= model && *p == '_'; p--)
|
||||
*p = 0;
|
||||
|
||||
/* Build ID string */
|
||||
if (vendor[0] && model[0] && serial[0])
|
||||
snprintf(id, len, "%s_%s_%s", vendor, model, serial);
|
||||
else if (model[0] && serial[0])
|
||||
snprintf(id, len, "%s_%s", model, serial);
|
||||
else if (serial[0])
|
||||
snprintf(id, len, "%s", serial);
|
||||
else
|
||||
snprintf(id, len, "%s", model);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Build disk path ID from devpath.
|
||||
* Convert /devices/pci0000:00/.../host0/.../0:0:0:0/block/sda
|
||||
* to pci-0000:00:1f.2-ata-1
|
||||
*/
|
||||
static int disk_path_id(struct uevent *ev, char *id, size_t len)
|
||||
{
|
||||
/* Simplified: just use the devpath hash for now */
|
||||
const char *p;
|
||||
|
||||
/* Find last component before block/ */
|
||||
p = strstr(ev->devpath, "/block/");
|
||||
if (!p)
|
||||
return -1;
|
||||
|
||||
/* Use subsystem and devname */
|
||||
snprintf(id, len, "%s-%s", ev->subsystem ?: "disk", ev->devname);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Create symlinks for block devices in /dev/disk/by-*.
|
||||
*/
|
||||
static int symlink_add_disk(struct uevent *ev)
|
||||
{
|
||||
char target[PATH_MAX], link[PATH_MAX], id[256];
|
||||
const char *name;
|
||||
|
||||
if (!ev->devname)
|
||||
return -1;
|
||||
|
||||
/* Get basename for relative link */
|
||||
name = strrchr(ev->devname, '/');
|
||||
name = name ? name + 1 : ev->devname;
|
||||
|
||||
/* by-id: serial-based identifier */
|
||||
if (!disk_serial_id(ev, id, sizeof(id))) {
|
||||
snprintf(target, sizeof(target), "../../%s", name);
|
||||
snprintf(link, sizeof(link), "/dev/disk/by-id/%s", id);
|
||||
symlink_create(target, link, ev->devpath);
|
||||
}
|
||||
|
||||
/* by-path: topology-based identifier */
|
||||
if (!disk_path_id(ev, id, sizeof(id))) {
|
||||
snprintf(target, sizeof(target), "../../%s", name);
|
||||
snprintf(link, sizeof(link), "/dev/disk/by-path/%s", id);
|
||||
symlink_create(target, link, ev->devpath);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Create symlinks for input devices in /dev/input/by-*.
|
||||
*/
|
||||
static int symlink_add_input(struct uevent *ev)
|
||||
{
|
||||
char target[PATH_MAX], link[PATH_MAX];
|
||||
char name[256], phys[256];
|
||||
char path[PATH_MAX];
|
||||
const char *devname;
|
||||
char *p;
|
||||
|
||||
if (!ev->devname)
|
||||
return -1;
|
||||
|
||||
devname = strrchr(ev->devname, '/');
|
||||
devname = devname ? devname + 1 : ev->devname;
|
||||
|
||||
/* Read device name */
|
||||
snprintf(path, sizeof(path), "/sys%s/device/name", ev->devpath);
|
||||
if (sysfs_read(path, name, sizeof(name)))
|
||||
return -1;
|
||||
|
||||
/* Clean up name */
|
||||
for (p = name; *p; p++) {
|
||||
if (*p == ' ' || *p == '/')
|
||||
*p = '_';
|
||||
}
|
||||
|
||||
/* by-id: name-based */
|
||||
snprintf(target, sizeof(target), "../%s", devname);
|
||||
snprintf(link, sizeof(link), "/dev/input/by-id/%s", name);
|
||||
symlink_create(target, link, ev->devpath);
|
||||
|
||||
/* by-path: physical path (if available) */
|
||||
snprintf(path, sizeof(path), "/sys%s/device/phys", ev->devpath);
|
||||
if (!sysfs_read(path, phys, sizeof(phys))) {
|
||||
for (p = phys; *p; p++) {
|
||||
if (*p == ' ' || *p == '/')
|
||||
*p = '_';
|
||||
}
|
||||
snprintf(link, sizeof(link), "/dev/input/by-path/%s", phys);
|
||||
symlink_create(target, link, ev->devpath);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Create appropriate symlinks based on device subsystem.
|
||||
*/
|
||||
int symlink_add(struct uevent *ev)
|
||||
{
|
||||
if (!ev->subsystem)
|
||||
return 0;
|
||||
|
||||
if (!strcmp(ev->subsystem, "block"))
|
||||
return symlink_add_disk(ev);
|
||||
|
||||
if (!strcmp(ev->subsystem, "input"))
|
||||
return symlink_add_input(ev);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove symlinks associated with a device.
|
||||
*/
|
||||
int symlink_del(struct uevent *ev)
|
||||
{
|
||||
struct dev_symlink *sl, *tmp;
|
||||
|
||||
if (!ev->devpath)
|
||||
return -1;
|
||||
|
||||
TAILQ_FOREACH_SAFE(sl, &symlinks, link, tmp) {
|
||||
if (!strcmp(sl->devpath, ev->devpath)) {
|
||||
unlink(sl->linkpath);
|
||||
logit(LOG_DEBUG, "Removed symlink %s", sl->linkpath);
|
||||
|
||||
TAILQ_REMOVE(&symlinks, sl, link);
|
||||
free(sl->devpath);
|
||||
free(sl->linkpath);
|
||||
free(sl);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Load firmware for a device.
|
||||
*
|
||||
* The kernel sends a uevent with FIRMWARE=filename when a driver
|
||||
* requests firmware via request_firmware(). We need to:
|
||||
* 1. Find the firmware file
|
||||
* 2. Write "1" to /sys/.../loading
|
||||
* 3. Write firmware data to /sys/.../data
|
||||
* 4. Write "0" to /sys/.../loading (or "-1" on error)
|
||||
*/
|
||||
int firmware_load(struct uevent *ev)
|
||||
{
|
||||
static const struct {
|
||||
const char *fmt;
|
||||
int kver; /* fmt has %s for uts.release before %s for firmware */
|
||||
} fw_paths[] = {
|
||||
{ "/lib/firmware/updates/%s/%s", 1 },
|
||||
{ "/lib/firmware/updates/%s", 0 },
|
||||
{ "/lib/firmware/%s/%s", 1 },
|
||||
{ "/lib/firmware/%s", 0 },
|
||||
{ "/usr/lib/firmware/updates/%s/%s", 1 },
|
||||
{ "/usr/lib/firmware/updates/%s", 0 },
|
||||
{ "/usr/lib/firmware/%s/%s", 1 },
|
||||
{ "/usr/lib/firmware/%s", 0 },
|
||||
};
|
||||
char fwpath[PATH_MAX], loading[PATH_MAX], data[PATH_MAX];
|
||||
struct utsname uts;
|
||||
char buf[4096];
|
||||
int fd_fw = -1, fd_data = -1;
|
||||
int ok = 0, found = 0;
|
||||
ssize_t n;
|
||||
size_t i;
|
||||
|
||||
if (!ev->firmware || !ev->devpath)
|
||||
return -1;
|
||||
|
||||
/*
|
||||
* Build sysfs paths up front so the fail label can always write
|
||||
* "-1" to abort the kernel firmware request, even if we never
|
||||
* managed to write "1" to start it.
|
||||
*/
|
||||
snprintf(loading, sizeof(loading), "/sys%s/loading", ev->devpath);
|
||||
snprintf(data, sizeof(data), "/sys%s/data", ev->devpath);
|
||||
|
||||
uname(&uts);
|
||||
|
||||
for (i = 0; i < NELEMS(fw_paths); i++) {
|
||||
if (fw_paths[i].kver)
|
||||
snprintf(fwpath, sizeof(fwpath), fw_paths[i].fmt,
|
||||
uts.release, ev->firmware);
|
||||
else
|
||||
snprintf(fwpath, sizeof(fwpath), fw_paths[i].fmt,
|
||||
ev->firmware);
|
||||
|
||||
if (fexist(fwpath)) {
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
logit(LOG_WARNING, "Firmware not found: %s", ev->firmware);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
logit(LOG_INFO, "Loading firmware %s from %s", ev->firmware, fwpath);
|
||||
|
||||
fd_fw = open(fwpath, O_RDONLY | O_CLOEXEC);
|
||||
if (fd_fw < 0) {
|
||||
logit(LOG_ERR, "Failed to open firmware %s: %s",
|
||||
fwpath, strerror(errno));
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (fnwrite("1", "%s", loading) < 0) {
|
||||
logit(LOG_ERR, "Failed to signal firmware loading start: %s",
|
||||
strerror(errno));
|
||||
goto fail;
|
||||
}
|
||||
|
||||
fd_data = open(data, O_WRONLY | O_CLOEXEC);
|
||||
if (fd_data < 0) {
|
||||
logit(LOG_ERR, "Failed to open %s: %s", data, strerror(errno));
|
||||
goto fail;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
ssize_t total;
|
||||
|
||||
n = read(fd_fw, buf, sizeof(buf));
|
||||
if (n == 0)
|
||||
break;
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
logit(LOG_ERR, "Failed reading firmware %s: %s",
|
||||
fwpath, strerror(errno));
|
||||
goto fail;
|
||||
}
|
||||
|
||||
for (total = 0; total < n; ) {
|
||||
ssize_t w = write(fd_data, buf + total, n - total);
|
||||
|
||||
if (w < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
logit(LOG_ERR, "Failed writing firmware data: %s",
|
||||
strerror(errno));
|
||||
goto fail;
|
||||
}
|
||||
total += w;
|
||||
}
|
||||
}
|
||||
|
||||
ok = 1;
|
||||
|
||||
fail:
|
||||
if (fd_fw >= 0)
|
||||
close(fd_fw);
|
||||
if (fd_data >= 0)
|
||||
close(fd_data);
|
||||
|
||||
if (fnwrite(ok ? "0" : "-1", "%s", loading) < 0)
|
||||
logit(LOG_WARNING, "Failed to signal firmware load %s: %s",
|
||||
ok ? "complete" : "abort", strerror(errno));
|
||||
else if (ok)
|
||||
logit(LOG_INFO, "Firmware %s loaded successfully", ev->firmware);
|
||||
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Load kernel module for a device based on modalias.
|
||||
*
|
||||
* Synchronous: waits for modprobe to complete before returning so
|
||||
* that rules running after `kmod load` (e.g. IMPORT{builtin}=blkid
|
||||
* once the filesystem driver is in) see the post-load state.
|
||||
* keventd's main loop uses SIGCHLD=SIG_IGN to auto-reap stray
|
||||
* children, so we temporarily restore the default handler here to
|
||||
* make waitpid() observable.
|
||||
*/
|
||||
int modprobe_load(const char *modalias)
|
||||
{
|
||||
struct sigaction sa_dfl, sa_old;
|
||||
pid_t pid;
|
||||
int status = 0;
|
||||
|
||||
if (!modalias)
|
||||
return -1;
|
||||
|
||||
logit(LOG_DEBUG, "Loading module for %s", modalias);
|
||||
|
||||
sigemptyset(&sa_dfl.sa_mask);
|
||||
sa_dfl.sa_flags = 0;
|
||||
sa_dfl.sa_handler = SIG_DFL;
|
||||
sigaction(SIGCHLD, &sa_dfl, &sa_old);
|
||||
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
logit(LOG_ERR, "fork failed: %s", strerror(errno));
|
||||
sigaction(SIGCHLD, &sa_old, NULL);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
execl("/sbin/modprobe", "modprobe", "-bq", modalias, NULL);
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
while (waitpid(pid, &status, 0) < 0 && errno == EINTR)
|
||||
;
|
||||
sigaction(SIGCHLD, &sa_old, NULL);
|
||||
|
||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Coldplug callback for nftw().
|
||||
* Writes "add" to each uevent file to trigger kernel to resend events.
|
||||
*/
|
||||
static int coldplug_cb(const char *path, const struct stat *st,
|
||||
int type, struct FTW *ftw)
|
||||
{
|
||||
size_t len;
|
||||
|
||||
(void)st;
|
||||
(void)ftw;
|
||||
|
||||
if (type != FTW_F)
|
||||
return 0;
|
||||
|
||||
len = strlen(path);
|
||||
if (len < 6)
|
||||
return 0;
|
||||
|
||||
/* Check if filename is "uevent" */
|
||||
if (strcmp(path + len - 6, "uevent"))
|
||||
return 0;
|
||||
|
||||
/* Trigger add event */
|
||||
fnwrite("add", "%s", path);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Trigger coldplug - replay device events for devices already present.
|
||||
*/
|
||||
int coldplug(void)
|
||||
{
|
||||
logit(LOG_INFO, "Starting coldplug...");
|
||||
|
||||
/* Walk /sys/devices and trigger uevents */
|
||||
if (nftw("/sys/devices", coldplug_cb, 64, FTW_PHYS) < 0) {
|
||||
logit(LOG_ERR, "Coldplug failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
logit(LOG_INFO, "Coldplug complete");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Local Variables:
|
||||
* indent-tabs-mode: t
|
||||
* c-file-style: "linux"
|
||||
* End:
|
||||
*/
|
||||
Reference in New Issue
Block a user