Files
Joachim Wiberg 59945b87f7 keventd: load udev rules in udev's order, with masking
The rules directories were read one after the other, each sorted on
its own.  udev instead sorts all files from all directories together
and lets a file mask one of the same name in a lower priority
directory.  The difference broke both standard override idioms
without a sound: copying a rules file to /etc/udev/rules.d/ to edit
it left the original running too, and symlinking a name to /dev/null
disabled nothing.

Collect everything with glob(), drop entries shadowed by a later
directory, and sort the survivors by filename, same as udev and the
tmpfiles.d handling in Finit itself.  The -r directory sits above
/etc/ so a test ruleset can override anything shipped.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-29 09:26:18 +02:00

852 lines
20 KiB
C

/* 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>
*
* 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"
#include <ctype.h>
#include <errno.h>
#include <dirent.h>
#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <linux/types.h>
#include <linux/netlink.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
# include <lite/lite.h>
#endif
#include <uev/uev.h>
#include "keventd.h"
#include "rules.h"
#include "sysfs.h"
#include "udevdb.h"
#include "cond.h"
#include "pid.h"
#include "util.h"
#define KEVENTD_VERSION "5.0"
#define _PATH_SYSFS_PWR "/sys/class/power_supply"
/* Default netlink group for uevent rebroadcast (libudev-zero convention) */
#define REBC_DEFAULT_NLGROUP 4
/* Used by settle and coldplug at startup */
struct coldplug_gate {
unsigned long long last_seq;
uint64_t stamp; /* kev_now_ms() of last change */
int primed;
};
/*
* 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;
}
uint64_t kev_now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
/*
* Track the kernel seqnum: returns 1 once it has been unchanged for
* stable_ms, 0 while still moving (or before the first sample).
*/
static int seqnum_stable(struct coldplug_gate *cg, int stable_ms)
{
unsigned long long cur = 0;
uint64_t now;
if (read_uevent_seqnum(&cur))
return 0;
now = kev_now_ms();
if (!cg->primed || cur != cg->last_seq) {
cg->last_seq = cur;
cg->stamp = now;
cg->primed = 1;
return 0;
}
return now - cg->stamp >= (uint64_t)stable_ms;
}
static int num_ac_online;
static int num_ac;
static int level;
static int logon;
static int passive; /* power-supply only, no device management */
static struct rule_list rules = TAILQ_HEAD_INITIALIZER(rules);
static char *rules_dir; /* extra rules dir from -r option */
int debug; /* debug in other modules as well */
void logit(int prio, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
if (logon)
vsyslog(prio, fmt, ap);
else if (prio <= level) {
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
}
va_end(ap);
}
#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)); }
/*
* Netlink rebroadcast support.
*
* The Linux kernel sends uevents to netlink multicast group 1 (bit 0)
* of NETLINK_KOBJECT_UEVENT. Only the device manager should listen on
* this raw kernel group. Userspace consumers (e.g., applications using
* libudev) expect to receive processed events on a separate group --
* conventionally group 4 (bit 2), established by systemd/udevd.
*
* libudev-zero (https://github.com/illiliti/libudev-zero), a daemonless
* replacement for libudev, listens on group 0x4 for these rebroadcast
* events. Without rebroadcast, graphical applications, Wayland/X11
* compositors, libinput, and anything else using libudev to monitor
* device hotplug will never see any events.
*
* Rebroadcast is enabled by default to group 0x4. Use -g to override
* the group mask, or -G to disable rebroadcast entirely. Bit 0 is
* always masked out to prevent a feedback loop with the kernel group.
*/
static int rebc_fd = -1;
static unsigned int rebc_nlgroups;
static void rebc_init(unsigned int nlgroups)
{
/* Mask out bit 0 (kernel group) to prevent feedback loop */
if (nlgroups & 1) {
logit(LOG_WARNING, "rebroadcast group mask 0x%x includes kernel group (bit 0), masking it out", nlgroups);
nlgroups &= ~1U;
}
if (!nlgroups) {
logit(LOG_WARNING, "no valid rebroadcast groups remaining, rebroadcast disabled");
return;
}
rebc_fd = socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT);
if (rebc_fd == -1) {
warn("failed creating rebroadcast socket");
return;
}
rebc_nlgroups = nlgroups;
logit(LOG_NOTICE, "rebroadcasting uevents to netlink group(s) 0x%x", nlgroups);
}
static void rebc_event(char *buf, size_t len)
{
struct sockaddr_nl sa = { 0 };
struct msghdr hdr = { 0 };
struct iovec iov;
if (rebc_fd == -1)
return;
iov.iov_base = buf;
iov.iov_len = len;
sa.nl_family = AF_NETLINK;
sa.nl_groups = rebc_nlgroups;
hdr.msg_name = &sa;
hdr.msg_namelen = sizeof(sa);
hdr.msg_iov = &iov;
hdr.msg_iovlen = 1;
if (sendmsg(rebc_fd, &hdr, 0) == -1)
logit(LOG_DEBUG, "rebroadcast failed: %s", strerror(errno));
}
static int check_online(const char *online)
{
int val;
if (!online)
return 0;
val = atoi(online);
logit(LOG_INFO, "AC %s", val ? "connected" : "disconnected");
return val;
}
static int is_ac(const char *type)
{
static const char *types[] = {
"Mains",
"USB",
"BrickID",
"Wireless",
NULL
};
int i;
for (i = 0; types[i]; i++) {
if (!strncmp(type, types[i], strlen(types[i])))
return 1;
}
return 0;
}
/*
* Handle power_supply change events (original keventd functionality).
*/
static void power_supply_change(const 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)
cond_emit(COND_SYS, "pwr/ac", 1);
num_ac_online++;
} else {
if (num_ac_online > 0)
num_ac_online--;
if (!num_ac_online)
cond_emit(COND_SYS, "pwr/ac", 0);
}
}
i += strlen(line) + 1;
}
}
/*
* True if rules have queued the kmod builtin for this event. Used to
* suppress the direct modprobe path when 80-drivers.rules will fork
* modprobe anyway -- otherwise coldplug runs modprobe twice per event.
*/
static int applied_has_kmod(const struct uevent *ev)
{
int i;
for (i = 0; i < ev->applied.nruncmds; i++) {
const char *cmd = ev->applied.run_cmds[i];
if (ev->applied.run_types[i] != RUN_BUILTIN)
continue;
if (!strncmp(cmd, "kmod", 4) &&
(cmd[4] == '\0' || isspace((unsigned char)cmd[4])))
return 1;
}
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.
*/
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);
/* Apply udev rules before built-in device handling */
if (!passive)
rules_apply(&rules, &ev);
switch (ev.action) {
case ACT_ADD:
if (!passive) {
/* Firmware loading takes priority */
if (ev.firmware)
firmware_load(&ev);
if (ev.modalias && !applied_has_kmod(&ev))
modprobe_load(ev.modalias);
/* Create device node if we have the info */
if (ev.major >= 0 && ev.minor >= 0 && ev.devname)
devnode_add(&ev);
/* Rename network interface if NAME= rule was applied */
if (ev.subsystem && !strcmp(ev.subsystem, "net"))
netdev_add(&ev);
/* Create symlinks */
symlink_add(&ev);
/* class/<subsystem>/<sysname> -- 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);
/* Remove device node, mirrors devnode_add(): net
* interfaces carry a devname but have no node. */
if (ev.major >= 0 && ev.minor >= 0 && 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:
if (!passive)
driver_cond(&ev, 1);
break;
case ACT_UNBIND:
if (!passive)
driver_cond(&ev, 0);
break;
default:
break;
}
/* Execute RUN+= commands from matched rules */
if (!passive)
rules_run_cmds(&ev);
/* Persist device properties to /run/udev/data/ */
if (!passive) {
if (ev.action == ACT_REMOVE)
udevdb_delete(&ev);
else
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);
}
static void init_power_supply(void)
{
struct dirent **d = NULL;
char path[384];
int i, n;
n = scandir(_PATH_SYSFS_PWR, &d, NULL, alphasort);
for (i = 0; i < n; i++) {
const char *nm = d[i]->d_name;
char buf[10];
snprintf(path, sizeof(path), "%s/%s/type", _PATH_SYSFS_PWR, nm);
if (!sysfs_read_file(path, buf, sizeof(buf)) && is_ac(buf)) {
num_ac++;
snprintf(path, sizeof(path), "%s/%s/online", _PATH_SYSFS_PWR, nm);
if (!sysfs_read_file(path, buf, sizeof(buf))) {
if (check_online(buf))
num_ac_online++;
}
}
free(d[i]);
}
if (n > 0)
free(d);
/* if any power_supply is online, or none can be found */
if (num_ac == 0 || num_ac_online > 0)
cond_emit(COND_SYS, "pwr/ac", 1);
}
static void disable_uevent_helper(void)
{
FILE *fp;
fp = fopen("/proc/sys/kernel/hotplug", "w");
if (!fp)
return;
fputs("\n", fp);
fclose(fp);
}
static void set_logging(int prio)
{
setlogmask(LOG_UPTO(prio));
level = prio;
}
static void sigusr1_cb(uev_t *w, void *arg, int events)
{
(void)w; (void)arg; (void)events;
debug ^= 1;
set_logging(debug ? LOG_DEBUG : LOG_NOTICE);
}
static void sigterm_cb(uev_t *w, void *arg, int events)
{
(void)arg; (void)events;
uev_exit(w->ctx);
}
static void sighup_cb(uev_t *w, void *arg, int events)
{
(void)w; (void)arg; (void)events;
kev_rules_reload();
}
static void sigchld_cb(uev_t *w, void *arg, int events)
{
(void)w; (void)arg; (void)events;
/* Reap all the children. */
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}
static void uevent_cb(uev_t *w, void *arg, int events)
{
char *uevent_buf = arg;
char rebc_buf[UEVENT_BUFFER_SIZE];
int len;
if (events & UEV_ERROR) {
warn("netlink watcher error, re-arming");
uev_io_start(w);
return;
}
len = recv(w->fd, uevent_buf, UEVENT_BUFFER_SIZE - 1, MSG_DONTWAIT);
if (len == -1) {
switch (errno) {
case EINTR:
case EAGAIN:
return;
case ENOBUFS:
warn("lost events, buffer overflow");
return;
default:
panic("recv failed");
}
}
uevent_buf[len] = 0;
/* Skip libudev events (start with "libudev") */
if (!strncmp(uevent_buf, "libudev", 7))
return;
/*
* Save raw buffer before handle_uevent() -- uevent_parse()
* modifies the buffer in-place (splits @ and = separators).
* Rebroadcast needs the original kernel format intact.
*/
if (rebc_fd != -1)
memcpy(rebc_buf, uevent_buf, len);
handle_uevent(uevent_buf, len);
/* Rebroadcast after processing so consumers see ready state. */
if (rebc_fd != -1)
rebc_event(rebc_buf, len);
}
/*
* Pidfile gate used with -c: defer pidfile() until the kernel's
* uevent_seqnum has been stable for 200ms, so that <pid/keventd>
* means "/dev is populated, persistent symlinks are live" rather
* than just "listening on netlink".
*/
unsigned long long kev_seq_processed(void)
{
return seq_processed;
}
static 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;
(void)events;
if (!seqnum_stable(cg, 200))
return;
pidfile(NULL);
logit(LOG_NOTICE, "keventd ready, coldplug queue drained");
uev_timer_stop(w);
}
/*
* Stop-gap "settle" mode: poll /sys/kernel/uevent_seqnum until it has
* been stable for `stable_ms` (default 200ms), or until `timeout_s`
* elapses. Imperfect -- a slow probe firing after we return still
* races -- but matches what typical `udevadm settle` users want.
*
* The proper fix lives in the IPC/queue work (see audit doc).
*/
static int cmd_settle(int timeout_s, int stable_ms)
{
struct coldplug_gate cg = { 0 };
uint64_t deadline = kev_now_ms() + (uint64_t)timeout_s * 1000;
while (1) {
if (seqnum_stable(&cg, stable_ms))
return 0;
if (!cg.primed) {
fprintf(stderr, "keventd: cannot read uevent_seqnum: %s\n",
strerror(errno));
return 1;
}
if (kev_now_ms() >= deadline)
return 1;
usleep(50000);
}
}
static int usage(int rc)
{
fprintf(stderr,
"Usage: keventd [-dGhnpSv] [-c] [-g GROUP] [-r DIR] [-t SECONDS]\n"
"\n"
"Options:\n"
" -c Run coldplug at startup\n"
" -d Enable debug mode (foreground, verbose)\n"
" -g GROUP Override netlink rebroadcast group (default: %d)\n"
" -G Disable netlink rebroadcast entirely\n"
" -h Show this help text\n"
" -n Run in foreground (no daemon)\n"
" -p Passive mode: power supply events only (no device management)\n"
" -r DIR Extra rules directory, overrides the standard udev paths\n"
" -S Settle: wait until kernel uevent queue is quiet, then exit\n"
" -t SEC Settle timeout in seconds (default: 30, with -S)\n"
" -v Show version\n"
"\n", REBC_DEFAULT_NLGROUP);
return rc;
}
/*
* Unified device manager daemon.
*
* Started by Finit as soon as possible when base filesystem is up,
* 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[])
{
uev_t netlink_watcher, sigusr1_watcher, sigterm_watcher, sighup_watcher, sigchld_watcher;
unsigned int nlgroups = REBC_DEFAULT_NLGROUP;
static char uevent_buf[UEVENT_BUFFER_SIZE];
static struct coldplug_gate cg;
struct sockaddr_nl nls = { 0 };
static uev_t coldplug_timer;
int settle_timeout = 30;
int do_coldplug = 0;
int foreground = 0;
int do_settle = 0;
uev_ctx_t ctx;
int nlfd;
int c;
/* 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, "cdg:Ghnpr:St:v")) != -1) {
switch (c) {
case 'c':
do_coldplug = 1;
break;
case 'd':
debug = 1;
foreground = 1;
break;
case 'g':
nlgroups = (unsigned int)atoi(optarg);
break;
case 'G':
nlgroups = 0;
break;
case 'h':
return usage(0);
case 'n':
foreground = 1;
break;
case 'p':
passive = 1;
break;
case 'r':
rules_dir = optarg;
break;
case 'S':
do_settle = 1;
break;
case 't':
settle_timeout = atoi(optarg);
break;
case 'v':
printf("keventd v%s\n", KEVENTD_VERSION);
return 0;
default:
return usage(1);
}
}
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);
set_logging(LOG_NOTICE);
logon = 1;
} else {
set_logging(debug ? LOG_DEBUG : LOG_NOTICE);
}
uev_init(&ctx);
uev_signal_init(&ctx, &sigusr1_watcher, sigusr1_cb, NULL, SIGUSR1);
uev_signal_init(&ctx, &sigterm_watcher, sigterm_cb, NULL, SIGTERM);
uev_signal_init(&ctx, &sighup_watcher, sighup_cb, NULL, SIGHUP);
uev_signal_init(&ctx, &sigchld_watcher, sigchld_cb, NULL, SIGCHLD);
/* Initialize condition directories */
init_power_supply();
/* Disable legacy kernel uevent helper; we own events via netlink.
* Skip in passive mode -- the hotplug daemon handles this. */
if (!passive)
disable_uevent_helper();
/* Set up netlink socket for kernel uevents */
nlfd = socket(PF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT);
if (nlfd == -1)
panic("failed creating netlink socket");
nls.nl_family = AF_NETLINK;
nls.nl_pid = 0;
nls.nl_groups = 1; /* Kernel uevents are on group 1 only */
if (bind(nlfd, (void *)&nls, sizeof(struct sockaddr_nl)))
panic("bind failed");
/* Increase receive buffer to reduce event loss */
{
int rcvbuf = 1024 * 1024;
setsockopt(nlfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
}
uev_io_init(&ctx, &netlink_watcher, uevent_cb, uevent_buf, nlfd, UEV_READ);
/* Initialize rebroadcast socket (default on, -G to disable).
* Skip in passive mode -- the hotplug daemon rebroadcasts. */
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();
/* Load udev rules from standard directories (and -r extra_dir) */
if (!passive)
rules_load_all(&rules, rules_dir);
/*
* With -c, defer pidfile until coldplug events have actually been
* drained, so <pid/keventd> means "/dev populated", not just
* "listening on netlink". Without -c, nothing is queued, drop
* the pidfile immediately as before.
*/
if (do_coldplug) {
uev_timer_init(&ctx, &coldplug_timer, coldplug_pidfile_cb, &cg, 100, 100);
logit(LOG_NOTICE, "keventd v%s started, draining coldplug queue...", KEVENTD_VERSION);
} else {
pidfile(NULL);
logit(LOG_NOTICE, "keventd v%s started, waiting for events...", KEVENTD_VERSION);
}
uev_run(&ctx, 0);
#ifdef HAVE_DBUS
devbus_exit();
#endif
if (rebc_fd != -1)
close(rebc_fd);
close(nlfd);
rules_free(&rules);
logit(LOG_NOTICE, "keventd shutting down");
return 0;
}
/**
* Local Variables:
* indent-tabs-mode: t
* c-file-style: "linux"
* End:
*/