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 .