From 292e87c0d1c22d293102b5378a3d368f38588cc0 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 16 Aug 2026 18:29:34 +0200 Subject: [PATCH] keventd: serve org.finit.Device1 on /run/keventd/bus The udev parity gaps that were blocked on IPC -- settle, trigger, info, queue introspection, runtime rule reload -- become bus methods. keventd serves its own socket the way Finit serves /run/finit/bus: brokerless, libink, one socket per daemon, no forwarding between the two. Settle(u) -> b parked until the queue drains or the timeout passes; true when settled Trigger(s, s) replay events, action + subsystem glob Info(s) -> a{ss} /run/udev/data properties for a devpath RulesReload() -> u re-read rules dirs, returns rule count QueueEmpty (b), SeqnumProcessed (t) properties DeviceProcessed (ss) signal after each fully handled event The queue state is the highest kernel seqnum keventd has handled, baselined at startup, against /sys/kernel/uevent_seqnum. keventd -S now asks the running daemon first and falls back to seqnum polling. In passive mode Trigger and RulesReload refuse. Adds link_call_connection() for the park bookkeeping. Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 7 + doc/dbus.md | 5 + doc/keventd.md | 61 ++++- keventd/Makefile.am | 9 + keventd/dbus.c | 587 ++++++++++++++++++++++++++++++++++++++++++++ keventd/keventd.c | 115 +++++++-- keventd/keventd.h | 16 ++ libink/dispatch.c | 1 + libink/link.h | 1 + man/keventd.8 | 32 +++ 10 files changed, 811 insertions(+), 23 deletions(-) create mode 100644 keventd/dbus.c diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index e9c9d8d9..fafcacdd 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -51,6 +51,13 @@ All relevant changes are documented in this file. - New `org.finit.Cond1` interface at `/org/finit/cond`: `Get`, `Set`, `Clear`, `List`, `Dump` for [user-defined conditions](conditions.md), with a `ConditionChanged (ss)` signal +- keventd serves `org.finit.Device1` on its own bus at + `/run/keventd/bus`: `Settle`, `Trigger`, `Info`, `RulesReload`, + queue-state properties, and a `DeviceProcessed (ss)` signal. The + `udevadm` settle/trigger/info equivalents are thereby bus methods + and `keventd -S` asks the running daemon first. Finit also gained a + `Manager1.ConfigReloaded` signal, fired when `initctl reload` + completes, for external condition providers and monitoring - Privileged D-Bus methods accept root and members of the `--with-group` group; everyone else is refused. On the local bus the caller's uid and groups come straight from the kernel diff --git a/doc/dbus.md b/doc/dbus.md index 505531c4..625609b4 100644 --- a/doc/dbus.md +++ b/doc/dbus.md @@ -22,6 +22,10 @@ Bus address | Local (always) | `unix:path=/run/finit/bus` | | System (opportunistic) | `unix:path=/var/run/dbus/system_bus_socket` | +The bundled device manager serves its own `org.finit.Device1` the same +way on `unix:path=/run/keventd/bus`, see [keventd](keventd.md); one +socket per daemon, no forwarding between them. + The **local** bus is brokerless: clients connect straight to Finit over a Unix-domain socket using the standard D-Bus SASL EXTERNAL handshake. No `dbus-daemon` is required, which makes it suitable for embedded systems that @@ -133,6 +137,7 @@ All read-only strings; observable via `Properties.Get` and |-----------------------|----------------------------------------|------------------------| | `ServiceStateChanged` | `sss` — identity, old state, new state | Service transitions | | `RunlevelChanged` | `ss` — old level, new level | System runlevel change | +| `ConfigReloaded` | — | `initctl reload` completed; external condition providers re-assert | State names emitted by `ServiceStateChanged` are stable wire strings: `halted`, `done`, `dead`, `cleanup`, `teardown`, `stopping`, `setup`, diff --git a/doc/keventd.md b/doc/keventd.md index 8810c2b0..9d923c48 100644 --- a/doc/keventd.md +++ b/doc/keventd.md @@ -40,6 +40,8 @@ kernel events and handles: system when device nodes appear or disappear - **Netlink rebroadcast**: rebroadcasts processed uevents to netlink group 0x4 for [libudev-zero][] consumers (enabled by default) +- **D-Bus interface**: `org.finit.Device1` -- settle, trigger, device + info, rules reload, and queue state over its own bus socket Device Nodes @@ -331,12 +333,15 @@ It is the `udevadm settle` equivalent for migration scenarios: keventd -S -t 10 && start-graphical-session -It does not talk to the running keventd (or any device manager) -- it -simply opens `/sys/kernel/uevent_seqnum` and polls until the kernel's -sequence counter has been stable for 200ms, then exits zero. After -the `-t SECONDS` timeout (default 30) it exits non-zero instead. -Because `uevent_seqnum` is maintained by the kernel itself, `-S` works -regardless of which device manager is active, or even if none is. +With D-Bus support it asks the running keventd over +[`Device1.Settle`](#d-bus-interface-orgfinitdevice1), which tracks the +event queue where the events actually flow. When no bus answers -- +another device manager, or a build without D-Bus -- it falls back to +polling `/sys/kernel/uevent_seqnum` until the kernel's sequence +counter has been stable for 200ms, then exits zero. After the +`-t SECONDS` timeout (default 30) it exits non-zero instead. The +fallback works regardless of which device manager is active, or even +if none is. Prefer the condition-based model (``, ``, ``) over settle when you control the service definition -- @@ -349,6 +354,50 @@ Debug logging can also be toggled at runtime by sending `SIGUSR1`: kill -USR1 $(pidof keventd) +D-Bus Interface (`org.finit.Device1`) +------------------------------------- + +With D-Bus support (default, `--disable-dbus` opts out) keventd serves +`org.finit.Device1` at `/org/finit/device` on its own brokerless bus, +`unix:path=/run/keventd/bus`, the same way Finit serves +[`org.finit`](dbus.md) on `/run/finit/bus`. There is no forwarding +between the two -- clients that want both connect to both. + +| Method | In sig | Out sig | Priv. | Notes | +|---------------|--------|---------|-------|----------------------------------------------| +| `Settle` | `u` | `b` | no | Wait until the event queue drains, timeout in seconds; `false` on timeout | +| `Trigger` | `ss` | — | yes | Replay events: action, subsystem glob (empty = all) | +| `Info` | `s` | `a{ss}` | no | `/run/udev/data` properties for a devpath | +| `RulesReload` | — | `u` | yes | Re-read the rules directories, returns rule count | + +| Property | Sig | Notes | +|-------------------|-----|-------------------------------------------| +| `QueueEmpty` | `b` | No device events in flight | +| `SeqnumProcessed` | `t` | Highest kernel seqnum keventd has handled | + +| Signal | Body | Fires when | +|-------------------|-------------------------|---------------------------------| +| `DeviceProcessed` | `ss` — devpath, action | An event has been fully handled: node, symlinks, database | + +Example, wait up to ten seconds for the queue to drain: + +```sh +dbus-send --address=unix:path=/run/keventd/bus \ + --type=method_call --print-reply --dest=org.finit \ + /org/finit/device org.finit.Device1.Settle uint32:10 +``` + +In passive mode (`-p`) `Trigger` and `RulesReload` refuse -- device +events are not handled here -- while `Settle` and the queue-state +properties remain meaningful. + +keventd's conditions are symlinks to Finit's `reconf` generation +marker, so they read the current generation by construction and never +enter `flux` -- like user-defined conditions, they need no reassert +after `initctl reload`. Device state does not change because Finit +re-read its configuration. + + Integration with Finit ---------------------- diff --git a/keventd/Makefile.am b/keventd/Makefile.am index 46f2c70f..85fcc886 100644 --- a/keventd/Makefile.am +++ b/keventd/Makefile.am @@ -9,6 +9,9 @@ keventd_SOURCES = keventd.c keventd.h uevent.c \ rules.c rules.h \ builtin.c builtin.h \ $(top_srcdir)/src/util.c $(top_srcdir)/src/util.h +# Per-target flags exist even when their conditional is false, so +# assign the base unconditionally and append under DBUS +keventd_CPPFLAGS = $(AM_CPPFLAGS) keventd_CFLAGS = -W -Wall -Wextra -std=gnu99 # Silence noisy false positives from PATH_MAX-into-PATH_MAX sysfs path # concatenations; real concerns (e.g. USB strings into small buffers) are @@ -21,6 +24,12 @@ keventd_LDADD = $(lite_LIBS) keventd_LDADD += $(uev_LIBS) keventd_LDADD += $(blkid_LIBS) +if DBUS +keventd_SOURCES += dbus.c +keventd_CPPFLAGS += -I$(top_srcdir)/libink +keventd_LDADD += $(top_builddir)/libink/libink.la +endif + # Curated udev rules derived from eudev 3.2.14 (GPL-2.0-or-later). # Installed to /lib/udev/rules.d/ so existing third-party rules and # local overrides under /etc/udev/rules.d/ coexist with standard udev diff --git a/keventd/dbus.c b/keventd/dbus.c new file mode 100644 index 00000000..8e727b64 --- /dev/null +++ b/keventd/dbus.c @@ -0,0 +1,587 @@ +/* keventd D-Bus interface: org.finit.Device1 on its own local bus + * + * keventd serves /run/keventd/bus the same way Finit serves + * /run/finit/bus: brokerless, libink, one socket per daemon. No + * message forwarding exists between the two, by design -- clients + * that want both connect to both. + * + * Copyright (c) 2026 Joachim Wiberg + * + * 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 "config.h" + +#ifdef HAVE_DBUS + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _LIBITE_LITE +# include +#else +# include +#endif + +#include "link.h" + +#include "keventd.h" +#include "udevdb.h" +#include "util.h" + +void logit(int prio, const char *fmt, ...); + +#define DEVBUS_DIR "/run/keventd" +#define DEVBUS_SOCKET DEVBUS_DIR "/bus" +#define DEVBUS_PATH_OBJECT "/org/finit/device" +#define DEVBUS_INTERFACE "org.finit.Device1" +#define DEVBUS_MAX_PEERS 16 +#define DEVBUS_MAX_SETTLES 8 + +struct peer { + uev_t watcher; + link_connection_t *conn; + int dead; + TAILQ_ENTRY(peer) link; +}; + +static TAILQ_HEAD(, peer) peers = TAILQ_HEAD_INITIALIZER(peers); +static TAILQ_HEAD(, peer) reapq = TAILQ_HEAD_INITIALIZER(reapq); +static link_server_t *server; +static uev_t accept_watcher; +static uev_t reap_timer; +static uev_ctx_t *devbus_ctx; +static size_t peer_count; +static int bus_passive; + +/* Parked Settle() calls with their own deadlines */ +struct settle { + link_connection_t *conn; + link_authz_t tok; + uint64_t deadline; /* CLOCK_MONOTONIC ms */ +}; + +static struct settle settles[DEVBUS_MAX_SETTLES]; +static uev_t settle_timer; +static int settle_armed; + +static uint64_t now_ms(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +/* + * Same policy as /run/finit/bus: root and members of the --with-group + * group. The group set comes from the kernel (SO_PEERGROUPS), the + * owning gid is resolved once at init so no call ever hits NSS. + */ +static gid_t privileged_gid = (gid_t)-1; /* DEFGROUP, resolved at init */ + +static int caller_is_privileged(uid_t uid, const gid_t *groups, int ngroups, + void *userdata) +{ + (void)userdata; + + if (uid == 0) + return 1; + + if (privileged_gid == (gid_t)-1) + return 0; /* no owning group to match against */ + + for (int i = 0; i < ngroups; i++) { + if (groups[i] == privileged_gid) + return 1; + } + + return 0; +} + +static void link_log_cb(void *userdata, const char *func, const char *msg) +{ + (void)userdata; + logit(LOG_DEBUG, "%s():%s", func, msg); +} + +/* ---------- peer lifecycle, same shape as src/dbus.c ---------- */ + +static void peer_reap(uev_t *w, void *arg, int events) +{ + struct peer *p; + + (void)w; (void)arg; (void)events; + while ((p = TAILQ_FIRST(&reapq))) { + TAILQ_REMOVE(&reapq, p, link); + link_connection_close(p->conn); + free(p); + } +} + +static void peer_drop(struct peer *p) +{ + int i; + + if (p->dead) + return; + p->dead = 1; + + uev_io_stop(&p->watcher); + TAILQ_REMOVE(&peers, p, link); + peer_count--; + + /* Settles parked on this connection die with it */ + for (i = 0; i < DEVBUS_MAX_SETTLES; i++) { + if (settles[i].conn == p->conn) + settles[i].conn = NULL; + } + + TAILQ_INSERT_TAIL(&reapq, p, link); + uev_timer_set(&reap_timer, 10, 0); +} + +static void peer_cb(uev_t *w, void *arg, int events) +{ + struct peer *p = arg; + + (void)w; + + if (UEV_ERROR == events || link_connection_process(p->conn) < 0) + peer_drop(p); +} + +static void accept_cb(uev_t *w, void *arg, int events) +{ + (void)arg; + + if (UEV_ERROR == events) { + logit(LOG_ERR, "D-Bus accept watcher error"); + return; + } + + for (;;) { + link_connection_t *conn = NULL; + struct peer *p; + + if (link_server_accept(server, &conn) < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) + logit(LOG_ERR, "Failed accepting D-Bus client: %s", + strerror(errno)); + break; + } + + if (peer_count >= DEVBUS_MAX_PEERS) { + logit(LOG_WARNING, "D-Bus peer cap reached (%zu), dropping", + peer_count); + link_connection_close(conn); + continue; + } + + p = calloc(1, sizeof(*p)); + if (!p) { + link_connection_close(conn); + continue; + } + + p->conn = conn; + TAILQ_INSERT_TAIL(&peers, p, link); + peer_count++; + + if (uev_io_init(w->ctx, &p->watcher, peer_cb, p, + link_connection_get_fd(conn), UEV_READ)) + peer_drop(p); + } +} + +/* ---------- parked Settle() bookkeeping ---------- */ + +static void settle_sweep(uev_t *w, void *arg, int events); + +/* + * The queue can only drain when an event is processed, and + * devbus_notify() sweeps on exactly that; the timer exists solely to + * enforce deadlines, so arm it one-shot for the nearest one. + */ +static void settle_arm(uint64_t deadline) +{ + uint64_t now = now_ms(); + int ms = deadline > now ? (int)(deadline - now) : 1; + + if (!settle_armed) { + uev_timer_init(devbus_ctx, &settle_timer, settle_sweep, + NULL, ms, 0); + settle_armed = 1; + } else { + uev_timer_set(&settle_timer, ms, 0); + } +} + +/* + * Resume parked settles: all of them when the queue drained (the + * handler re-runs, sees the empty queue, replies true), expired ones + * otherwise (the handler re-runs, sees link_call_resumed(), replies + * false). + */ +static void settle_sweep(uev_t *w, void *arg, int events) +{ + uint64_t next = 0; + uint64_t now = now_ms(); + int empty = kev_queue_empty(); + int i; + + (void)w; (void)arg; (void)events; + + for (i = 0; i < DEVBUS_MAX_SETTLES; i++) { + struct settle *s = &settles[i]; + + if (!s->conn) + continue; + + if (empty || now >= s->deadline) { + link_connection_t *conn = s->conn; + link_authz_t tok = s->tok; + + s->conn = NULL; + link_call_resume(conn, tok); + continue; + } + if (!next || s->deadline < next) + next = s->deadline; + } + + if (next) { + settle_arm(next); + } else if (settle_armed) { + uev_timer_stop(&settle_timer); + settle_armed = 0; + } +} + +/* ---------- org.finit.Device1 ---------- */ + +static int device1_settle(link_call_t *call, void *u) +{ + link_writer_t *w; + uint32_t timeout; + int i; + + int empty; + + (void)u; + if (link_call_read_u32(call, &timeout) < 0) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (u)"); + + empty = kev_queue_empty(); + if (!empty && !link_call_resumed(call)) { + for (i = 0; i < DEVBUS_MAX_SETTLES; i++) { + if (settles[i].conn) + continue; + + if (link_call_park(call, &settles[i].tok)) + break; + + settles[i].conn = link_call_connection(call); + settles[i].deadline = now_ms() + (uint64_t)timeout * 1000; + settle_arm(settles[i].deadline); + return 0; + } + /* no room to wait: answer with the state as it stands */ + } + + w = link_call_reply(call); + link_w_bool(w, empty); + return 0; +} + +static int device1_trigger(link_call_t *call, void *u) +{ + const char *action, *glob; + + (void)u; + if (link_call_read_string(call, &action) < 0 || + link_call_read_string(call, &glob) < 0) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s, s)"); + + if (bus_passive) + return link_call_reply_error(call, + "org.finit.Error.Failed", + "keventd is passive, device events are not handled here"); + + if (*action && !uevent_action_valid(action)) + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "unknown uevent action"); + + if (coldplug_trigger(action, glob)) + return link_call_reply_error(call, + "org.finit.Error.Failed", "trigger failed"); + + (void)link_call_reply(call); + return 0; +} + +static int device1_info(link_call_t *call, void *u) +{ + struct uevent ev; + const char *devpath; + link_writer_t *w; + int i; + + (void)u; + if (link_call_read_string(call, &devpath) < 0 || *devpath != '/') + return link_call_reply_error(call, + "org.freedesktop.DBus.Error.InvalidArgs", + "expected (s), an absolute devpath"); + + memset(&ev, 0, sizeof(ev)); + if (udevdb_read_devpath(devpath, &ev)) { + uevent_env_free(&ev); + return link_call_reply_error(call, + "org.finit.Error.NoSuchDevice", + "no database record for devpath"); + } + + w = link_call_reply(call); + link_w_array_begin(w, '{'); + for (i = 0; i < ev.nenv; i++) { + link_w_struct_begin(w); + link_w_string(w, ev.env_key[i]); + link_w_string(w, ev.env_val[i]); + link_w_struct_end(w); + } + link_w_array_end(w); + + uevent_env_free(&ev); + return 0; +} + +static int device1_rules_reload(link_call_t *call, void *u) +{ + link_writer_t *w; + + (void)u; + if (bus_passive) + return link_call_reply_error(call, + "org.finit.Error.Failed", + "keventd is passive, rules are not applied here"); + + w = link_call_reply(call); + link_w_u32(w, (uint32_t)kev_rules_reload()); + return 0; +} + +static int prop_queue_empty(link_writer_t *w, void *u) +{ + (void)u; + link_w_bool(w, kev_queue_empty()); + return 0; +} + +static int prop_seqnum(link_writer_t *w, void *u) +{ + (void)u; + link_w_u64(w, kev_seq_processed()); + return 0; +} + +static const link_method_t device_methods[] = { + { .name = "Settle", .in_sig = "u", .out_sig = "b", + .handler = device1_settle }, + { .name = "Trigger", .in_sig = "ss", .out_sig = "", + .flags = LINK_METHOD_PRIVILEGED, .handler = device1_trigger }, + { .name = "Info", .in_sig = "s", .out_sig = "a{ss}", + .handler = device1_info }, + { .name = "RulesReload", .in_sig = "", .out_sig = "u", + .flags = LINK_METHOD_PRIVILEGED, .handler = device1_rules_reload }, + { NULL, NULL, NULL, 0, NULL } +}; + +static const link_property_t device_properties[] = { + { .name = "QueueEmpty", .sig = "b", .getter = prop_queue_empty }, + { .name = "SeqnumProcessed", .sig = "t", .getter = prop_seqnum }, + { NULL, NULL, NULL } +}; + +static const link_signal_t device_signals[] = { + { .name = "DeviceProcessed", .sig = "ss" }, + { NULL, NULL } +}; + +static const link_vtable_t device_vtable = { + .interface = DEVBUS_INTERFACE, + .methods = device_methods, + .properties = device_properties, + .signals = device_signals, +}; + +/* ---------- signal emission + settle nudge ---------- */ + +static void devbus_emit_signal(const char *member, const char *sig, + const uint8_t *body, size_t len) +{ + struct peer *p, *tmp; + + TAILQ_FOREACH_SAFE(p, &peers, link, tmp) { + if (link_connection_emit_signal(p->conn, DEVBUS_PATH_OBJECT, + DEVBUS_INTERFACE, member, + sig, body, len) >= 0) + continue; + if (errno == EMSGSIZE || errno == EINVAL) + break; /* nothing on the wire, same for all */ + peer_drop(p); + } +} + +void devbus_notify(const char *devpath, const char *action) +{ + uint8_t body[512]; + link_writer_t w; + ssize_t blen; + + if (!server) + return; + + /* A processed event may have been the last one in flight */ + if (settle_armed) + settle_sweep(NULL, NULL, 0); + + if (TAILQ_EMPTY(&peers) || !devpath) + return; + + link_writer_init(&w, body, sizeof(body)); + link_w_string(&w, devpath); + link_w_string(&w, action ?: ""); + blen = link_writer_finish(&w); + if (blen < 0) + return; + + devbus_emit_signal("DeviceProcessed", "ss", body, (size_t)blen); +} + +/* ---------- init / exit / settle client ---------- */ + +int devbus_init(uev_ctx_t *ctx, int passive) +{ + devbus_ctx = ctx; + bus_passive = passive; + + link_set_logger(link_log_cb, NULL); + + if (mkpath(DEVBUS_DIR, 0755) && errno != EEXIST) { + logit(LOG_ERR, "Failed creating %s: %s", DEVBUS_DIR, + strerror(errno)); + return -1; + } + + if (link_server_new(&server, DEVBUS_SOCKET, 0660) < 0) { + logit(LOG_ERR, "Failed creating D-Bus socket %s: %s", + DEVBUS_SOCKET, strerror(errno)); + return -1; + } + + /* Resolve the owning group once, see caller_is_privileged() */ + privileged_gid = getgroup(DEFGROUP); + if (chown(DEVBUS_SOCKET, geteuid(), privileged_gid)) + logit(LOG_WARNING, "Failed setting group %s on %s: %s", + DEFGROUP, DEVBUS_SOCKET, strerror(errno)); + + link_server_set_authorizer(server, caller_is_privileged, NULL); + + if (link_server_add_object(server, DEVBUS_PATH_OBJECT, + &device_vtable, NULL) < 0) { + logit(LOG_ERR, "Failed registering %s", DEVBUS_INTERFACE); + link_server_free(server); + server = NULL; + return -1; + } + + uev_timer_init(ctx, &reap_timer, peer_reap, NULL, 0, 0); + + if (uev_io_init(ctx, &accept_watcher, accept_cb, NULL, + link_server_get_fd(server), UEV_READ)) { + logit(LOG_ERR, "Failed watching D-Bus socket"); + link_server_free(server); + server = NULL; + return -1; + } + + logit(LOG_NOTICE, "Serving %s on %s", DEVBUS_INTERFACE, DEVBUS_SOCKET); + return 0; +} + +void devbus_exit(void) +{ + struct peer *p; + + if (!server) + return; + + while ((p = TAILQ_FIRST(&peers))) + peer_drop(p); + peer_reap(NULL, NULL, 0); + + uev_io_stop(&accept_watcher); + link_server_free(server); + server = NULL; +} + +/* + * Client side of Settle, for `keventd -S`: ask the running keventd. + * Returns 0 settled, 1 timeout, -1 when no bus answers (fall back to + * polling the kernel seqnum). + */ +int devbus_client_settle(int timeout_s) +{ + link_client_t *c; + link_reader_t r; + const link_reply_t *reply; + int rc, settled = 0; + + c = link_client_open(DEVBUS_SOCKET); + if (!c) + return -1; + + rc = link_client_call_v(c, DEVBUS_PATH_OBJECT, DEVBUS_INTERFACE, + "Settle", "u", (uint32_t)timeout_s); + if (rc != LINK_CALL_OK) { + link_client_close(c); + return -1; + } + + reply = link_client_reply(c); + if (reply && reply->body) { + link_reader_init(&r, reply->body, reply->body_len); + link_r_bool(&r, &settled); + } + link_client_close(c); + + return settled ? 0 : 1; +} + +#endif /* HAVE_DBUS */ diff --git a/keventd/keventd.c b/keventd/keventd.c index a779a50b..191519bd 100644 --- a/keventd/keventd.c +++ b/keventd/keventd.c @@ -34,6 +34,8 @@ * THE SOFTWARE. */ +#include "config.h" + #include #include #include @@ -303,6 +305,26 @@ static int applied_has_kmod(const struct uevent *ev) return 0; } +/* + * Queue bookkeeping for the D-Bus Settle/QueueEmpty surface: the + * highest kernel seqnum keventd has fully handled. The baseline is + * taken at bus init, events from before keventd started are not ours + * to wait for. + */ +static unsigned long long seq_processed; + +static void note_seqnum(const char *seqnum) +{ + unsigned long long seq; + + if (!seqnum) + return; + + seq = strtoull(seqnum, NULL, 10); + if (seq > seq_processed) + seq_processed = seq; +} + /* * Handle a single uevent from the kernel. */ @@ -395,6 +417,12 @@ static void handle_uevent(char *buf, size_t len) udevdb_write(&ev); } + /* Fully handled: only now may QueueEmpty/Settle count it */ + note_seqnum(ev.seqnum); +#ifdef HAVE_DBUS + devbus_notify(ev.devpath, uevent_action_str(ev.action)); +#endif + uevent_env_free(&ev); } @@ -485,8 +513,7 @@ static void sigterm_cb(uev_t *w, void *arg, int events) static void sighup_cb(uev_t *w, void *arg, int events) { (void)w; (void)arg; (void)events; - rules_free(&rules); - rules_load_all(&rules, rules_dir); + kev_rules_reload(); } static void sigchld_cb(uev_t *w, void *arg, int events) @@ -550,24 +577,66 @@ static void uevent_cb(uev_t *w, void *arg, int events) * means "/dev is populated, persistent symlinks are live" rather * than just "listening on netlink". */ +/* + * Read the kernel's uevent sequence counter. Shared by the coldplug + * gate, settle mode, and the D-Bus queue state. + */ +static int read_uevent_seqnum(unsigned long long *out) +{ + FILE *fp; + int rc; + + fp = fopen("/sys/kernel/uevent_seqnum", "r"); + if (!fp) + return -1; + + rc = fscanf(fp, "%llu", out) == 1 ? 0 : -1; + fclose(fp); + + return rc; +} + +unsigned long long kev_seq_processed(void) +{ + return seq_processed; +} + +void kev_seq_baseline(void) +{ + unsigned long long seq; + + if (!read_uevent_seqnum(&seq) && seq > seq_processed) + seq_processed = seq; +} + +int kev_queue_empty(void) +{ + unsigned long long seq; + + if (read_uevent_seqnum(&seq)) + return 1; /* no counter, nothing to wait for */ + + return seq <= seq_processed; +} + +int kev_rules_reload(void) +{ + rules_free(&rules); + return rules_load_all(&rules, rules_dir); +} + static void coldplug_pidfile_cb(uev_t *w, void *arg, int events) { struct coldplug_gate *cg = arg; unsigned long long cur = 0; struct timespec now; - FILE *fp; long dt_ms; (void)events; - fp = fopen("/sys/kernel/uevent_seqnum", "r"); - if (!fp) + if (read_uevent_seqnum(&cur)) return; - if (fscanf(fp, "%llu", &cur) != 1) - cur = cg->last_seq; - fclose(fp); - clock_gettime(CLOCK_MONOTONIC, &now); if (!cg->primed) { @@ -610,17 +679,11 @@ static int cmd_settle(int timeout_s, int stable_ms) last_change = start; while (1) { - FILE *fp; - - fp = fopen("/sys/kernel/uevent_seqnum", "r"); - if (!fp) { + if (read_uevent_seqnum(&cur_seq)) { fprintf(stderr, "keventd: cannot read uevent_seqnum: %s\n", strerror(errno)); return 1; } - if (fscanf(fp, "%llu", &cur_seq) != 1) - cur_seq = last_seq; - fclose(fp); clock_gettime(CLOCK_MONOTONIC, &now); @@ -736,8 +799,17 @@ int main(int argc, char *argv[]) } } - if (do_settle) + if (do_settle) { +#ifdef HAVE_DBUS + /* Ask the running keventd, which tracks the queue where + * the events flow; fall back to polling the kernel's + * seqnum when no bus is up. */ + int rc = devbus_client_settle(settle_timeout); + if (rc >= 0) + return rc; +#endif return cmd_settle(settle_timeout, 200); + } if (!foreground) { openlog("keventd", LOG_PID, LOG_DAEMON); @@ -787,6 +859,12 @@ int main(int argc, char *argv[]) if (nlgroups && !passive) rebc_init(nlgroups); +#ifdef HAVE_DBUS + /* Bus up before coldplug so Settle covers the coldplug drain */ + kev_seq_baseline(); + devbus_init(&ctx, passive); +#endif + /* Run coldplug if requested */ if (do_coldplug) coldplug(); @@ -811,6 +889,9 @@ int main(int argc, char *argv[]) uev_run(&ctx, 0); +#ifdef HAVE_DBUS + devbus_exit(); +#endif if (rebc_fd != -1) close(rebc_fd); close(nlfd); diff --git a/keventd/keventd.h b/keventd/keventd.h index d48f28a8..ad045733 100644 --- a/keventd/keventd.h +++ b/keventd/keventd.h @@ -163,6 +163,22 @@ int coldplug (void); int coldplug_trigger (const char *action, const char *subsys_glob); int uevent_action_valid(const char *action); +/* keventd.c: queue and rules state, for the D-Bus layer */ +unsigned long long kev_seq_processed(void); +void kev_seq_baseline (void); +int kev_queue_empty (void); +int kev_rules_reload (void); + +#ifdef HAVE_DBUS +#include + +/* dbus.c: org.finit.Device1 on /run/keventd/bus */ +int devbus_init (uev_ctx_t *ctx, int passive); +void devbus_exit (void); +void devbus_notify (const char *devpath, const char *action); +int devbus_client_settle(int timeout_s); +#endif + #endif /* FINIT_KEVENTD_H_ */ /** diff --git a/libink/dispatch.c b/libink/dispatch.c index df320e76..db80d0b6 100644 --- a/libink/dispatch.c +++ b/libink/dispatch.c @@ -261,6 +261,7 @@ const char *link_call_path (const link_call_t *c) { return c ? c->incoming.p const char *link_call_interface(const link_call_t *c) { return c ? c->incoming.interface : NULL; } const char *link_call_member (const link_call_t *c) { return c ? c->incoming.member : NULL; } uid_t link_call_uid (const link_call_t *c) { return c ? c->uid : LINK_UID_UNKNOWN; } +link_connection_t *link_call_connection(const link_call_t *c) { return c ? c->conn : NULL; } link_writer_t *link_call_reply(link_call_t *call) { diff --git a/libink/link.h b/libink/link.h index 73e42270..da5466b7 100644 --- a/libink/link.h +++ b/libink/link.h @@ -293,6 +293,7 @@ const char *link_call_path (const link_call_t *call); const char *link_call_interface(const link_call_t *call); const char *link_call_member (const link_call_t *call); uid_t link_call_uid (const link_call_t *call); +link_connection_t *link_call_connection(const link_call_t *call); /* ---------- reading method-call arguments ---------- * diff --git a/man/keventd.8 b/man/keventd.8 index 1eac4ddb..a3e21fee 100644 --- a/man/keventd.8 +++ b/man/keventd.8 @@ -215,6 +215,38 @@ service cron { command = "cron -f" } .Ed +.Sh D-BUS INTERFACE +With D-Bus support, +.Nm +serves +.Cm org.finit.Device1 +at +.Pa /org/finit/device +on its own brokerless bus, +.Pa unix:path=/run/keventd/bus , +the same way +.Xr finit 8 +serves +.Cm org.finit . +Methods: +.Bl -tag -width RulesReload -offset indent -compact +.It Cm Settle (u) -> b +Wait until the device event queue drains, timeout in seconds +.It Cm Trigger (ss) +Replay events: action and subsystem glob, empty glob matches all +.It Cm Info (s) -> a{ss} +Device properties for a devpath, from /run/udev/data +.It Cm RulesReload () -> u +Re-read the udev rules directories, returns the rule count +.El +.Pp +Properties +.Cm QueueEmpty (b) +and +.Cm SeqnumProcessed (t) +expose the queue state; the +.Cm DeviceProcessed (ss) +signal fires after each fully handled event. .Sh NETLINK REBROADCAST The Linux kernel sends uevents to netlink multicast group 1 (bit 0) of .Dv NETLINK_KOBJECT_UEVENT .