From f3461ce2baaddd168a2d23ad5fcfaecdb2409490 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 30 May 2026 16:18:29 +0200 Subject: [PATCH] keventd: add class// and driver/ conditions dev/ only fires when a device gets a /dev node, which leaves a lot of embedded-relevant hardware uncoverable: DSA switch cores, IIO sensors, LEDs, backlight, PHYs, regulators -- all live purely under sysfs. Two new condition namespaces: class// asserted on every sysfs class device add (e.g. ) driver/ asserted while the driver is bound to at least one device (e.g. ) A driver can bind to several devices, so driver/ conditions are refcounted: asserted on first bind, cleared when the last device is unbound. dev_cond() is generalized into a static cond_emit(prefix, rel, set) so class_cond() and driver_cond() share the same mkpath + symlink/erase code. The name avoids colliding with src/cond.h's public cond_path() helper (unrelated function that returns a condition's filesystem path). Signed-off-by: Joachim Wiberg --- doc/conditions.md | 10 ++++- doc/keventd.md | 51 ++++++++++++++++++++++ keventd/keventd.c | 13 +++++- keventd/keventd.h | 3 ++ keventd/uevent.c | 106 +++++++++++++++++++++++++++++++++++++++++----- src/cond.h | 4 ++ 6 files changed, 174 insertions(+), 13 deletions(-) diff --git a/doc/conditions.md b/doc/conditions.md index e61687bb..c56dab18 100644 --- a/doc/conditions.md +++ b/doc/conditions.md @@ -177,7 +177,8 @@ Built-in Conditions Finit comes with a set of plugins for conditions: - - `keventd`: provides `` and `` + - `keventd`: provides ``, ``, ``, and + `` - `devmon` (built-in fallback for `` without keventd) - `netlink`: provides `` - `pidfile`: provides `` @@ -191,6 +192,11 @@ condition is cleared. If keventd is not in use (an external device manager like udevd is used instead), the `devmon` built-in provides the same conditions by monitoring `/dev` and `/dev/dir` with inotify. +keventd also asserts `class//` for devices that have +no `/dev` node, e.g. LEDs and DSA switch ports, and `driver/` +while a driver is bound to at least one device. See +[keventd](keventd.md) for details. + The `pidfile` plugin recursively watches `/run/` for PID files created by the monitored services, and sets a corresponding condition in the `pid/` namespace. @@ -234,6 +240,8 @@ Built-in conditions: - `usr/foo` - `boot/arg` - `dev/node` and `dev/dir/node` +- `class//` +- `driver/` > [!NOTE] > Here, `up` means administratively up, the interface flag `IFF_UP`. diff --git a/doc/keventd.md b/doc/keventd.md index 4a3cd576..b44b4258 100644 --- a/doc/keventd.md +++ b/doc/keventd.md @@ -227,9 +227,60 @@ devices: command = "/usr/sbin/gps-daemon" } +Network interfaces are not device nodes and do not live in the `dev/` +namespace -- a `/dev/wan` node created by a user must not be confused +with a `wan` interface. To wait for an interface, use the `netlink` +plugin's `net//exist` condition, plus `net//up` (admin +up) and `net//running` (carrier present) to gate on link state: + + service dhcpcd { + description = "DHCP client" + runlevel = "2345" + conditions = { "net/wan/exist" } + command = "/usr/sbin/dhcpcd" + } + +keventd provides the parallel `class/net/` condition, like for +any other sysfs class device. + When the device is removed, the condition is cleared and Finit stops the dependent services. +### Class Conditions (`class/`) + +Many devices live in sysfs without a `/dev/` node -- DSA switch ports, +IIO sensors, LEDs, backlight, PHYs, regulators. For those, keventd +asserts `class//` on every `add` event so services +can still wait for them: + + service blink-blue { + description = "LED driver" + runlevel = "2345" + conditions = { "class/leds/blue" } + command = "/usr/sbin/blink-blue" + } + +The condition is cleared on `remove`. + +### Driver Conditions (`driver/`) + +`driver/` is asserted while the driver `` is bound to at +least one device, from the kernel's `bind`/`unbind` uevents. Use this +to gate on slow-probing hardware whose readiness isn't marked by a +class device or `/dev` node, such as a switch core or complex PHY: + + service dsa-probe { + description = "DSA topology probe" + runlevel = "2345" + conditions = { "driver/mt7530" } + command = "/usr/sbin/dsa-probe" + } + +A driver bound to several devices keeps the condition asserted until +the last device is unbound. To wait for one specific device instance, +gate on what its probe creates instead: the `class/` condition or +`/dev` node of the child device. + ### Power Supply Conditions (`sys/pwr/`) keventd monitors the `power_supply` subsystem and provides: diff --git a/keventd/keventd.c b/keventd/keventd.c index 0868a1bc..70ff19d3 100644 --- a/keventd/keventd.c +++ b/keventd/keventd.c @@ -334,11 +334,17 @@ static void handle_uevent(char *buf, size_t len) /* Create symlinks */ symlink_add(&ev); + + /* class// -- covers sysfs-only + * devices that don't get a /dev node. */ + class_cond(&ev, 1); } break; case ACT_REMOVE: if (!passive) { + class_cond(&ev, 0); + /* Remove symlinks first */ symlink_del(&ev); @@ -356,8 +362,13 @@ static void handle_uevent(char *buf, size_t len) break; case ACT_BIND: + if (!passive) + driver_cond(&ev, 1); + break; + case ACT_UNBIND: - /* Driver bind/unbind - could trigger conditions */ + if (!passive) + driver_cond(&ev, 0); break; default: diff --git a/keventd/keventd.h b/keventd/keventd.h index e96733e8..d6554a72 100644 --- a/keventd/keventd.h +++ b/keventd/keventd.h @@ -149,6 +149,9 @@ int devnode_add (struct uevent *ev); int devnode_del (struct uevent *ev); int netdev_add (struct uevent *ev); +void class_cond (const struct uevent *ev, int set); +void driver_cond (const struct uevent *ev, int set); + int symlink_add (struct uevent *ev); int symlink_del (struct uevent *ev); void symlink_write_db (const char *devpath, FILE *fp); diff --git a/keventd/uevent.c b/keventd/uevent.c index 10a3d021..6f7ba7c5 100644 --- a/keventd/uevent.c +++ b/keventd/uevent.c @@ -58,37 +58,121 @@ void logit(int prio, const char *fmt, ...); /* - * Set/clear a device condition by creating/removing a symlink. + * Set/clear a Finit 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. + * + * prefix is the namespace path fragment (COND_DEV, COND_CLASS, + * COND_DRIVER, ...) ending in '/'. rel is the relative path under that + * namespace (e.g. "sda", "input/event0", "net/eth0", "mt7530"). */ -static void dev_cond(const char *devname, int set) +static void cond_emit(const char *prefix, const char *rel, int set) { char cond[PATH_MAX]; char *dir; - if (!devname) + if (!prefix || !rel || !*rel) return; - snprintf(cond, sizeof(cond), "%s%s", _PATH_CONDDEV, devname); + snprintf(cond, sizeof(cond), "%s%s%s", _PATH_COND, prefix, rel); - /* Create parent directory if needed (e.g., dev/input/) */ + /* mkpath is idempotent (EEXIST-tolerant) so just always call it. */ 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 (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); + logit(LOG_WARNING, "Failed setting %s%s condition", prefix, rel); } else { if (erase(cond) && errno != ENOENT) - logit(LOG_WARNING, "Failed clearing dev/%s condition", devname); + logit(LOG_WARNING, "Failed clearing %s%s condition", prefix, rel); } } +static void dev_cond(const char *devname, int set) +{ + cond_emit(COND_DEV, devname, set); +} + +/* + * class// -- fires for any sysfs class device add, + * regardless of whether the device gets a /dev node. Lets services + * depend on sysfs-only hardware (DSA switch ports, LEDs, IIO sensors, + * PHYs, etc.). + */ +void class_cond(const struct uevent *ev, int set) +{ + const char *sysname; + char rel[256]; + + if (!ev->subsystem || !ev->devpath) + return; + + sysname = strrchr(ev->devpath, '/'); + sysname = sysname ? sysname + 1 : ev->devpath; + if (!*sysname) + return; + + snprintf(rel, sizeof(rel), "%s/%s", ev->subsystem, sysname); + cond_emit(COND_CLASS, rel, set); +} + +/* + * driver/ -- asserted while the driver is bound to at least one + * device, from the kernel's bind/unbind uevents. Lets services depend + * on a specific driver having probed its hardware (e.g. ) + * even when no class device or /dev node marks the moment. + * + * A driver can bind to several devices, so the condition is refcounted: + * asserted on the first bind, cleared when the last device is unbound. + */ +struct drv_bind { + TAILQ_ENTRY(drv_bind) link; + char *name; + size_t count; +}; + +static TAILQ_HEAD(, drv_bind) drv_binds = TAILQ_HEAD_INITIALIZER(drv_binds); + +void driver_cond(const struct uevent *ev, int set) +{ + struct drv_bind *db; + + if (!ev->driver) + return; + + TAILQ_FOREACH(db, &drv_binds, link) { + if (!strcmp(db->name, ev->driver)) + break; + } + + if (set) { + if (!db) { + db = calloc(1, sizeof(*db)); + if (!db) + goto emit; + db->name = strdup(ev->driver); + if (!db->name) { + free(db); + goto emit; + } + TAILQ_INSERT_TAIL(&drv_binds, db, link); + } + if (db->count++) + return; + } else if (db) { + if (--db->count) + return; + TAILQ_REMOVE(&drv_binds, db, link); + free(db->name); + free(db); + } +emit: + cond_emit(COND_DRIVER, ev->driver, set); +} + /* Symlink tracking for cleanup on device removal */ static TAILQ_HEAD(, dev_symlink) symlinks = TAILQ_HEAD_INITIALIZER(symlinks); diff --git a/src/cond.h b/src/cond.h index 0ba5c2da..5c5e3af4 100644 --- a/src/cond.h +++ b/src/cond.h @@ -7,12 +7,16 @@ #define COND_BASE "finit/cond" #define COND_DEV "dev/" +#define COND_DRIVER "driver/" +#define COND_CLASS "class/" #define COND_PID "pid/" #define COND_SYS "sys/" #define COND_USR "usr/" #define _PATH_COND _PATH_VARRUN COND_BASE "/" #define _PATH_CONDDEV _PATH_COND COND_DEV +#define _PATH_CONDDRIVER _PATH_COND COND_DRIVER +#define _PATH_CONDCLASS _PATH_COND COND_CLASS #define _PATH_CONDPID _PATH_COND COND_PID #define _PATH_CONDSYS _PATH_COND COND_SYS #define _PATH_CONDUSR _PATH_COND COND_USR