keventd: add class/<subsystem>/<name> and driver/<name> conditions

dev/<X> 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/<subsystem>/<sysname>  asserted on every sysfs class device add
                                (e.g. <class/leds/blue>)
  driver/<name>                 asserted while the driver is bound to at
                                least one device (e.g. <driver/mt7530>)

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 <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg committed 2026-08-16 22:03:34 +02:00
1 parent e4d9193f06
commit f3461ce2ba
6 files changed
+174 -13

No files matched your search

+9 -1
View File
@@ -177,7 +177,8 @@ Built-in Conditions
Finit comes with a set of plugins for conditions: Finit comes with a set of plugins for conditions:
- `keventd`: provides `<dev/...>` and `<sys/pwr/...>` - `keventd`: provides `<dev/...>`, `<class/...>`, `<driver/...>`, and
`<sys/pwr/...>`
- `devmon` (built-in fallback for `<dev/...>` without keventd) - `devmon` (built-in fallback for `<dev/...>` without keventd)
- `netlink`: provides `<net/...>` - `netlink`: provides `<net/...>`
- `pidfile`: provides `<pid/...>` - `pidfile`: provides `<pid/...>`
@@ -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 manager like udevd is used instead), the `devmon` built-in provides the
same conditions by monitoring `/dev` and `/dev/dir` with inotify. same conditions by monitoring `/dev` and `/dev/dir` with inotify.
keventd also asserts `class/<subsystem>/<name>` for devices that have
no `/dev` node, e.g. LEDs and DSA switch ports, and `driver/<name>`
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 The `pidfile` plugin recursively watches `/run/` for PID files created
by the monitored services, and sets a corresponding condition in the by the monitored services, and sets a corresponding condition in the
`pid/` namespace. `pid/` namespace.
@@ -234,6 +240,8 @@ Built-in conditions:
- `usr/foo` - `usr/foo`
- `boot/arg` - `boot/arg`
- `dev/node` and `dev/dir/node` - `dev/node` and `dev/dir/node`
- `class/<subsystem>/<name>`
- `driver/<name>`
> [!NOTE] > [!NOTE]
> Here, `up` means administratively up, the interface flag `IFF_UP`. > Here, `up` means administratively up, the interface flag `IFF_UP`.
+51
View File
@@ -227,9 +227,60 @@ devices:
command = "/usr/sbin/gps-daemon" 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/<iface>/exist` condition, plus `net/<iface>/up` (admin
up) and `net/<iface>/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/<iface>` condition, like for
any other sysfs class device.
When the device is removed, the condition is cleared and Finit stops When the device is removed, the condition is cleared and Finit stops
the dependent services. 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/<subsystem>/<sysname>` 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/<name>` is asserted while the driver `<name>` 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/`) ### Power Supply Conditions (`sys/pwr/`)
keventd monitors the `power_supply` subsystem and provides: keventd monitors the `power_supply` subsystem and provides:
+12 -1
View File
@@ -334,11 +334,17 @@ static void handle_uevent(char *buf, size_t len)
/* Create symlinks */ /* Create symlinks */
symlink_add(&ev); symlink_add(&ev);
/* class/<subsystem>/<sysname> -- covers sysfs-only
* devices that don't get a /dev node. */
class_cond(&ev, 1);
} }
break; break;
case ACT_REMOVE: case ACT_REMOVE:
if (!passive) { if (!passive) {
class_cond(&ev, 0);
/* Remove symlinks first */ /* Remove symlinks first */
symlink_del(&ev); symlink_del(&ev);
@@ -356,8 +362,13 @@ static void handle_uevent(char *buf, size_t len)
break; break;
case ACT_BIND: case ACT_BIND:
if (!passive)
driver_cond(&ev, 1);
break;
case ACT_UNBIND: case ACT_UNBIND:
/* Driver bind/unbind - could trigger conditions */ if (!passive)
driver_cond(&ev, 0);
break; break;
default: default:
+3
View File
@@ -149,6 +149,9 @@ int devnode_add (struct uevent *ev);
int devnode_del (struct uevent *ev); int devnode_del (struct uevent *ev);
int netdev_add (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_add (struct uevent *ev);
int symlink_del (struct uevent *ev); int symlink_del (struct uevent *ev);
void symlink_write_db (const char *devpath, FILE *fp); void symlink_write_db (const char *devpath, FILE *fp);
+95 -11
View File
@@ -58,37 +58,121 @@
void logit(int prio, const char *fmt, ...); 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 * keventd is a standalone daemon, so we manipulate the filesystem directly
* rather than using Finit's internal cond_set()/cond_clear() API. * 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 cond[PATH_MAX];
char *dir; char *dir;
if (!devname) if (!prefix || !rel || !*rel)
return; 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 = strdupa(cond);
dir = dirname(dir); dir = dirname(dir);
if (strcmp(dir, _PATH_CONDDEV)) { if (mkpath(dir, 0755) && errno != EEXIST)
if (mkpath(dir, 0755) && errno != EEXIST) logit(LOG_WARNING, "Failed creating condition dir %s", dir);
logit(LOG_WARNING, "Failed creating condition dir %s", dir);
}
if (set) { if (set) {
if (symlink(_PATH_RECONF, cond) && errno != EEXIST) 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 { } else {
if (erase(cond) && errno != ENOENT) 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/<subsystem>/<sysname> -- 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/<name> -- 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. <driver/mt7530>)
* 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 */ /* Symlink tracking for cleanup on device removal */
static TAILQ_HEAD(, dev_symlink) symlinks = TAILQ_HEAD_INITIALIZER(symlinks); static TAILQ_HEAD(, dev_symlink) symlinks = TAILQ_HEAD_INITIALIZER(symlinks);
+4
View File
@@ -7,12 +7,16 @@
#define COND_BASE "finit/cond" #define COND_BASE "finit/cond"
#define COND_DEV "dev/" #define COND_DEV "dev/"
#define COND_DRIVER "driver/"
#define COND_CLASS "class/"
#define COND_PID "pid/" #define COND_PID "pid/"
#define COND_SYS "sys/" #define COND_SYS "sys/"
#define COND_USR "usr/" #define COND_USR "usr/"
#define _PATH_COND _PATH_VARRUN COND_BASE "/" #define _PATH_COND _PATH_VARRUN COND_BASE "/"
#define _PATH_CONDDEV _PATH_COND COND_DEV #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_CONDPID _PATH_COND COND_PID
#define _PATH_CONDSYS _PATH_COND COND_SYS #define _PATH_CONDSYS _PATH_COND COND_SYS
#define _PATH_CONDUSR _PATH_COND COND_USR #define _PATH_CONDUSR _PATH_COND COND_USR