Merge pull request #491 from finit-project/devman

Finit 5: udev/eudev replacement

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg authored and GitHub committed 2026-08-18 10:37:14 +02:00
commit 48e85efb88
94 files changed
+9744 -605

No files matched your search

+41 -2
View File
@@ -26,7 +26,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get -y update
sudo apt-get -y install pkg-config libconfuse-dev clang
sudo apt-get -y install pkg-config libconfuse-dev clang libblkid-dev
# clang picks the newest gcc tree it finds and needs the
# matching libstdc++ headers to link the fuzzer runtime
sudo apt-get -y install libstdc++-14-dev || true
@@ -99,7 +99,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get -y update
sudo apt-get -y install pkg-config tree jq libcap-dev libconfuse-dev
sudo apt-get -y install pkg-config tree jq libcap-dev libconfuse-dev libblkid-dev
wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz
wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz
tar xf libuev-2.4.1.tar.xz
@@ -149,3 +149,42 @@ jobs:
with:
name: finit-test-${{ matrix.compiler }}
path: test/*.log
no-dbus:
# The D-Bus support is default-enabled, so the HAVE_DBUS paths in
# initctl and finit only bit-rot silently without this leg.
name: no-dbus
runs-on: ubuntu-latest
if: github.event_name != 'push' || github.ref == 'refs/heads/master'
steps:
- name: Install dependencies
run: |
sudo apt-get -y update
sudo apt-get -y install pkg-config tree jq libcap-dev libconfuse-dev libblkid-dev
wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz
wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz
tar xf libuev-2.4.1.tar.xz
tar xf libite-2.6.2.tar.gz
(cd libuev-2.4.1 && ./configure && make -j9 && sudo make install-strip)
(cd libite-2.6.2 && ./configure && make -j9 && sudo make install-strip)
sudo ldconfig
- uses: actions/checkout@v7
- name: Build without D-Bus
run: |
./autogen.sh
./configure --prefix=/usr --exec-prefix= --sysconfdir=/etc --localstatedir=/var \
--disable-dbus --enable-testserv-plugin --with-keventd
make -j9 V=1
- name: Verify no bus artifacts
run: |
DESTDIR=/tmp make install-strip
! strings /tmp/sbin/initctl | grep -q finit/bus
! strings /tmp/sbin/finit | grep -q org.finit
- name: Enable unprivileged userns (unshare)
run: |
sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0
- name: Smoke test
run: |
make -C test setup-chroot
make -j1 -C test check TESTS='start-stop-serv.sh' \
|| (cat test/test-suite.log; false)
+1 -1
View File
@@ -55,7 +55,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get -y update
sudo apt-get -y install pkg-config libcap-dev libconfuse-dev
sudo apt-get -y install pkg-config libcap-dev libconfuse-dev libblkid-dev
wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz
wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz
tar xf libuev-2.4.1.tar.xz
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
- name: Installing dependencies ...
run: |
sudo apt-get -y update
sudo apt-get -y install pkg-config jq libcap-dev libconfuse-dev
sudo apt-get -y install pkg-config jq libcap-dev libconfuse-dev libblkid-dev
wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz
wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz
tar xf libuev-2.4.1.tar.xz
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get -y update
sudo apt-get -y install pkg-config jq libcap-dev libconfuse-dev
sudo apt-get -y install pkg-config jq libcap-dev libconfuse-dev libblkid-dev
wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz
wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz
tar xf libuev-2.4.1.tar.xz
+5
View File
@@ -11,6 +11,11 @@ if DBUS
SUBDIRS += libink dbus-1
endif
SUBDIRS += src system tmpfiles.d
if KEVENTD
SUBDIRS += keventd
endif
dist_doc_DATA = README.md LICENSE contrib/finit.conf
if CONTRIB
+13 -4
View File
@@ -18,6 +18,7 @@ AC_CONFIG_FILES([Makefile
man/Makefile
plugins/Makefile
src/Makefile
keventd/Makefile
system/Makefile system/10-hotplug.conf
test/test.env test/Makefile
test/lib/Makefile test/src/Makefile
@@ -124,7 +125,7 @@ AC_PLUGIN([resolvconf], [no], [Setup necessary files for resolvconf])
AC_PLUGIN([x11-common], [no], [Console setup (for X)])
AC_PLUGIN([netlink], [yes], [Basic netlink plugin for IFUP/IFDN and GW events. Can be replaced with externally built plugin that links with libnl or similar.])
AC_PLUGIN([hook-scripts], [no], [Trigger script execution from hook points])
AC_PLUGIN([hotplug], [yes], [Start udevd or mdev kernel event datamon])
AC_PLUGIN([hotplug], [no], [Start udevd or mdev kernel event datamon])
AC_PLUGIN([rtc], [yes], [Save and restore RTC using hwclock])
AC_PLUGIN([tty], [yes], [Automatically activate new TTYs, e.g. USB-to-serial])
AC_PLUGIN([urandom], [yes], [Setup and save random seed at boot/shutdown])
@@ -177,7 +178,10 @@ AC_ARG_WITH(random-seed,
[random_seed=$withval], [random_seed=yes])
AC_ARG_WITH(keventd,
AS_HELP_STRING([--with-keventd], [Enable built-in keventd, default: no]),, [with_keventd=no])
AS_HELP_STRING([--with-keventd], [Enable built-in keventd device manager, default: yes]),, [with_keventd=yes])
AC_ARG_WITH(udev-rules,
AS_HELP_STRING([--without-udev-rules], [Skip install of curated udev rules under /lib/udev/rules.d, default: yes when keventd enabled]),, [with_udev_rules=yes])
AC_ARG_WITH(sulogin,
AS_HELP_STRING([--with-sulogin@<:@=USER@:>@], [Enable built-in sulogin, optional USER to request password for (default root), default: no.]),[sulogin=$withval],[with_sulogin=no])
@@ -328,7 +332,11 @@ AS_IF([test "x$rtc_file" != "xno"], [
AC_DEFINE_UNQUOTED(RTC_FILE, "$rtcfile_path", [Save and restore system time from this file if /dev/rtc is missing.])],[
AC_DEFINE_UNQUOTED(RTC_FILE, NULL)])
AS_IF([test "x$with_keventd" != "xno"], [with_keventd=yes])
AS_IF([test "x$with_keventd" != "xno"], [
with_keventd=yes
PKG_CHECK_MODULES([blkid], [blkid],, [
AC_MSG_ERROR([libblkid (util-linux) is required to build keventd.
Install the dev package (e.g. libblkid-dev) or disable keventd with --without-keventd])])])
AS_IF([test "x$with_sulogin" != "xno"], [
AS_IF([test "x$sulogin" = "xyes"], [
@@ -361,6 +369,7 @@ AS_IF([test "x$with_plugin_path" != "xno"], [
AM_CONDITIONAL(BASH, [test "x$bash_dir" != "xno"])
AM_CONDITIONAL(STATIC, [test "x$enable_static" = "xyes"])
AM_CONDITIONAL(KEVENTD, [test "x$with_keventd" != "xno"])
AM_CONDITIONAL(UDEV_RULES, [test "x$with_keventd" != "xno" -a "x$with_udev_rules" != "xno"])
AM_CONDITIONAL(SULOGIN, [test "x$with_sulogin" != "xno"])
AM_CONDITIONAL(WATCHDOGD, [test "x$with_watchdog" != "xno"])
AM_CONDITIONAL(LIBSYSTEMD, [test "x$with_libsystemd" != "xno"])
@@ -444,7 +453,7 @@ Optional features:
Install doc/..........: $enable_doc
Install contrib/......: $enable_contrib
Bash completion.......: $bash_completion_dir
Built-in keventd......: $with_keventd
Built-in keventd......: $with_keventd (blkid: $blkid_LIBS)
Built-in sulogin......: $with_sulogin $sulogin
Built-in watchdogd....: $with_watchdog $watchdog
Built-in logrotate....: $enable_logrotate
+37 -31
View File
@@ -8,6 +8,12 @@ All relevant changes are documented in this file.
### Changes
- The generated `/run/finit/system/*.conf` files for built-in services
(keventd, watchdogd, runparts) are now read before
`/lib/finit/system/*.conf` instead of after. Built-ins can now be
referenced in `if:` statements from system .conf files and overridden
by name from both `/lib/finit/system` and `/etc/finit.d`, the latter
is still always read last
- The `dbus.so` plugin, which starts an external `dbus-daemon`, is now
enabled by default. It does nothing on systems without a
`dbus-daemon` installed, and `--disable-dbus-plugin` opts out. The
@@ -33,38 +39,38 @@ All relevant changes are documented in this file.
private bus at `/run/finit/bus`, and -- opportunistically --
registering `org.finit` on the standard system bus when a
`dbus-daemon` is reachable. No external `libdbus`/`sd-bus`/`GIO`
dependency.
The bus implements the stock `org.freedesktop.DBus`,
`org.freedesktop.DBus.Peer`, `org.freedesktop.DBus.Introspectable`,
and `org.freedesktop.DBus.Properties` interfaces, plus three
Finit-specific ones:
* `org.finit.Manager1` at `/org/finit/manager` --
`ListServices`, `GetService`, `Start`/`Stop`/`Restart`/`Reload`,
`SetRunlevel`, `SetDebug`, `Signal`, `Suspend`, and the
`Reboot`/`Halt`/`Poweroff` triplet. Read-only properties
`Runlevel`, `PrevRunlevel`, `Version`. Signals
`ServiceStateChanged (sss)` and `RunlevelChanged (ss)`.
* `org.finit.Service1` at `/org/finit/service/<encoded>` -- one
object per loaded service, with `Start`/`Stop`/`Restart`/`Reload`
for working off an object handle rather than passing the
identity string around.
* `org.finit.Cond1` at `/org/finit/cond` -- `Get`, `Set`, `Clear`,
`List`, `Dump` for [user-defined conditions](conditions.md),
with a `ConditionChanged (ss)` signal.
Privileged 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 (`SO_PEERCRED` +
`SO_PEERGROUPS`), so the check never blocks PID 1 on an NSS lookup.
Over the system bus the uid is resolved from the broker
(`GetConnectionUnixUser`) and privileged methods are root-only.
Read-only methods are open.
dependency. The bus implements the stock `org.freedesktop.DBus`,
`Peer`, `Introspectable`, and `Properties` interfaces.
See [D-Bus Integration](dbus.md) for the full surface, build flag,
and `dbus-send`/`dbus-monitor` examples.
and `dbus-send`/`dbus-monitor` examples
- New `org.finit.Manager1` interface at `/org/finit/manager`:
`ListServices`, `GetService`, `Start`/`Stop`/`Restart`/`Reload`,
`SetRunlevel`, `SetDebug`, `Signal`, `Suspend`, and the
`Reboot`/`Halt`/`Poweroff` triplet, which take a timeout argument
arming the emergency shutdown bypass like `initctl -t`. Read-only
properties `Runlevel`, `PrevRunlevel`, `Version`; signals
`ServiceStateChanged (sss)` and `RunlevelChanged (ss)`
- New `org.finit.Service1` interface at `/org/finit/service/<encoded>`,
one object per loaded service, with `Start`/`Stop`/`Restart`/`Reload`
for working off an object handle rather than passing the identity
string around
- 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
(`SO_PEERCRED` + `SO_PEERGROUPS`), so the check never blocks PID 1
on an NSS lookup. Over the system bus the uid is resolved from the
broker (`GetConnectionUnixUser`) and privileged methods are
root-only. Read-only methods are open
- `initctl` now transparently routes through D-Bus when the bus is
reachable, with the legacy `INIT_SOCKET` transport as a fallback:
+2 -1
View File
@@ -3,9 +3,10 @@ EXTRA_DIST = AUTHORS ChangeLog.md
docsdir := @docdir@/doc
dist_docs_DATA = build.md cmdline.md commands.md conditions.md \
dbus.md \
distro.md example.md features.md index.md initctl.md \
keventd.md plugins.md requirements.md runlevels.md \
runparts.md service.md signals.md state-machine.md \
watchdog.md
udev-matching.md watchdog.md
dist_docs_DATA += img/logo.png img/svc-machine.png img/svc-machine.svg \
img/alpine-screenshot2.png
+13
View File
@@ -8,6 +8,13 @@ optional plugins to enable. It depends on three external libraries:
- [libite][] (-lite), much needed frog DNA
- [libConfuse][], the parser behind the .conf format
The bundled device manager, keventd, needs a fourth: libblkid from
[util-linux][]. It reads the filesystem UUID and label off block
devices, which is what the curated rules name the `/dev/disk/by-uuid/`
and `/dev/disk/by-label/` symlinks after. keventd is enabled by
default, so this is a hard requirement unless you build with
`--without-keventd`.
> [!IMPORTANT]
> Most free/open source software packages that use `configure` default
> to install to `/usr/local`. However, some Linux distributions do no
@@ -24,6 +31,7 @@ version of the above mentioned libraries. Currently required versions:
- libite v2.2.0, or later
- libuEv v2.2.0, or later
- libConfuse v3.3, or later
- libblkid, any version, when keventd is enabled
Configure
@@ -67,6 +75,10 @@ Below are a few of the main switches to configure:
* `--enable-x11-common-plugin`: Enable the optional X Window `x11-common.so` plugin.
* `--without-keventd`: Drop the bundled device manager, for systems that
run mdev, mdevd, or udevd instead. Enabled by default, and the only
thing that pulls in libblkid. See [Bundled Device Manager](keventd.md).
* `--with-sulogin`: Enable bundled `sulogin` program. Default is to use the
system `sulogin(8)`. The sulogin shipped with Finit *allows password-less*
login if the `root` user is disabled or has no password at all.
@@ -259,3 +271,4 @@ it only for debugging start up issues when Finit crashes.
[libuEv]: https://github.com/troglobit/libuev
[libite]: https://github.com/troglobit/libite
[libConfuse]: https://github.com/libconfuse/libconfuse
[util-linux]: https://github.com/util-linux/util-linux
+18 -6
View File
@@ -177,15 +177,25 @@ Built-in Conditions
Finit comes with a set of plugins for conditions:
- `devmon` (built-in)
- `netlink`
- `pidfile`
- `keventd`: provides `<dev/...>`, `<class/...>`, `<driver/...>`, and
`<sys/pwr/...>`
- `devmon` (built-in fallback for `<dev/...>` without keventd)
- `netlink`: provides `<net/...>`
- `pidfile`: provides `<pid/...>`
- `sys`
- `usr`
The `devmon` (built-in) plugin monitors `/dev` and `/dev/dir` for device
nodes being created and removed. It is active only when a run, task, or
service has declared a `<dev/foo>` or `<dev/dir/bar>` condition.
The `dev/` conditions are provided by `keventd`, the built-in device
manager. When keventd creates a device node in `/dev`, it also asserts
the corresponding `dev/` condition. When a device is removed, the
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/<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
by the monitored services, and sets a corresponding condition in the
@@ -230,6 +240,8 @@ Built-in conditions:
- `usr/foo`
- `boot/arg`
- `dev/node` and `dev/dir/node`
- `class/<subsystem>/<name>`
- `driver/<name>`
> [!NOTE]
> Here, `up` means administratively up, the interface flag `IFF_UP`.
+18 -14
View File
@@ -24,8 +24,8 @@ the following layout is recommended:
| `- ...
`- run/
`- finit/
|- bus # Finit's own D-Bus socket, see doc/dbus.md
`- system/
|- dbus.conf
|- keventd.conf
|- runparts.conf
|- watchdogd.conf
@@ -35,18 +35,21 @@ Configuration files in `/etc` are provided by the user, or projects like
[finit-skel](https://github.com/finit-project/finit-skel) and extended by
the user.
The files in `/run/finit/system/*.conf` are generated by Finit for its
built-in services: runparts, [watchdog](../watchdog.md), and
[`keventd`](../keventd.md), if they are enabled. They are read before
the other .conf directories, so any later file can reference them in
`if:` statements and override them by name.
The files in `/lib/finit/system/*.conf` are system critical services and
setup provided by Finit, e.g. udev/mdev that must run very early at system
bootstrap. This system directory was introduced in Finit v4.4 to replace
the hard-coded services provided by plugins before. All .conf files in this
directory be either replaced by a system administrator or overridden by a
file with the same name in `/etc/finit.d/`.
the hard-coded services provided by plugins before.
The files in `/run/finit/system/*.conf` are created by plugins and Finit
bundled services like runparts, [watchdog](../watchdog.md), and
[`keventd`](../keventd.md) if they are enabled. Like
`/lib/finit/system/*.conf`, these files can be overridden by file with
the same name in `/etc/finit.d/`.
All .conf files in both directories can be either replaced by a system
administrator or overridden by a file with the same name in
`/etc/finit.d/`, which is always read last -- copy a .conf there and
modify it, or symlink it to `/dev/null` to disable it outright.
Services in the `available/` and `enabled/` sub-directories are called
dynamic services, in contrast to static services -- the only difference
@@ -55,16 +58,17 @@ with the `enable` and `disable` commands. An administrator can always
create files and symlinks manually.
At bootstrap, and `initctl reload`, all .conf files are read, starting
with `finit.conf`, then `/lib/finit/system/*.conf`, `finit.d/*.conf`,
and finally all `finit.d/enabled/*.conf` files. Each directory is a
unique group, where files within each group are sorted alphabetically.
with `finit.conf`, then `/run/finit/system/*.conf`,
`/lib/finit/system/*.conf`, `finit.d/*.conf`, and finally all
`finit.d/enabled/*.conf` files. Each directory is a unique group,
where files within each group are sorted alphabetically.
**Example:**
/run/finit/system/keventd.conf
/run/finit/system/runparts.conf
/lib/finit/system/10-hotplug.conf
/lib/finit/system/90-testserv.conf
/run/finit/system/dbus.conf
/run/finit/system/runparts.conf
/etc/finit.d/10-abc.conf
/etc/finit.d/20-abc.conf
/etc/finit.d/enabled/1-aaa.conf
+4 -2
View File
@@ -125,8 +125,10 @@ Load kernel modules, each with optional arguments. Similar to the
Deprecated, there is both a `modules-load.so` and a `modprobe.so` plugin
that can handle module loading better. The former supports loading from
`/etc/modules-load.d/`, the latter uses kernel modinfo to automatically
load (or coldplug) every required module. For hotplug we recommend the
BusyBox mdev tool, add to `/etc/mdev.conf`:
load (or coldplug) every required module. Hotplug module loading is
handled by [keventd](../keventd.md), the built-in device manager. On
systems using the hotplug plugin with BusyBox mdev instead, add to
`/etc/mdev.conf`:
$MODALIAS=.* root:root 0660 @modprobe -b "$MODALIAS"
+44 -8
View File
@@ -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
@@ -33,6 +37,17 @@ The socket is `0660`, owned by `root` and the group given to
does, so restricting one and not the other would leave the door open.
Members of that group may use it, see [Authorization](#authorization).
Finit accepts at most 64 concurrent local-bus peers; further
connections are refused until one disconnects. Match rules given to
`AddMatch` support the `type`, `interface`, `member`, and `path` keys;
`arg0` and its variants are not implemented.
For the system bus a standard busconfig policy ships in
`dbus-1/org.finit.conf`: only root may own `org.finit`, unprivileged
callers reach introspection, properties, and the read-only methods.
Finit also enforces per-method authorization itself, so a permissive
policy installed by mistake does not open state-changing methods.
The **system** bus is best-effort: Finit probes for a running `dbus-daemon`
and, when reachable, claims the well-known name `org.finit` so that standard
tooling sees Finit just like any other system service:
@@ -67,7 +82,7 @@ Every node implements the usual stock interfaces:
| Interface | Purpose |
|---------------------------------------|-------------------------------------------|
| `org.freedesktop.DBus` | `Hello`, `AddMatch`, `RemoveMatch` |
| `org.freedesktop.DBus` | `Hello`, `AddMatch`, `RemoveMatch` — on `/org/freedesktop/DBus` only |
| `org.freedesktop.DBus.Peer` | `Ping`, `GetMachineId` |
| `org.freedesktop.DBus.Introspectable` | `Introspect()` — XML description |
| `org.freedesktop.DBus.Properties` | `Get`, `GetAll`; nothing is writable |
@@ -91,11 +106,19 @@ and the service registry.
| `Stop` | `s` | — | yes | Stop the service(s) matching the identity |
| `Restart` | `s` | — | yes | Restart (stop + start) the service(s) |
| `Reload` | — | — | yes | Re-read all `*.conf` and apply changes |
| `SetRunlevel` | `u` | — | yes | Transition to runlevel `u` (0–6) |
| `SetRunlevel` | `u` | — | yes | Transition to runlevel `u` (0–9) |
| `SetDebug` | — | — | yes | Toggle Finit's runtime debug flag |
| `Signal` | `su` | — | yes | Send signal `u` (1–31) to services matching identity `s` |
| `Suspend` | — | — | yes | `sync()` + suspend-to-RAM |
| `Reboot`, `Halt`, `Poweroff` | — | — | yes | Trigger the corresponding shutdown sequence |
| `Reboot`, `Halt`, `Poweroff` | `u` | — | yes | Trigger the corresponding shutdown sequence; the argument arms the emergency fallback timer (seconds, 0 = none) |
Edge cases follow the legacy `initctl` protocol: `Suspend` and the
reboot family reply `WrongRunlevel` during bootstrap and shutdown,
`SetRunlevel` is a warning + no-op in runlevel 0/6 and records the
level for the end of bootstrap in runlevel S, and `Signal` fails for
stopped services. One deliberate divergence: a `SetRunlevel` argument
outside 0–9 returns `InvalidArgs`, where the legacy protocol
acknowledges silently.
### Properties
@@ -114,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; providers of generation-file conditions re-assert |
State names emitted by `ServiceStateChanged` are stable wire strings:
`halted`, `done`, `dead`, `cleanup`, `teardown`, `stopping`, `setup`,
@@ -135,7 +159,7 @@ constructing it by hand.
| `Start` | — | — | yes | Equivalent to `Manager1.Start(<identity>)` for this service |
| `Stop` | — | — | yes | … |
| `Restart` | — | — | yes | … |
| `Reload` | — | — | yes | Reload (SIGHUP if supported, else restart) |
| `Reload` | — | — | yes | Send the service's `reload-signal`, default `SIGHUP`; restart when set to `"none"` |
### Properties
@@ -195,6 +219,18 @@ Lives at **`/org/finit/cond`**. Exposes Finit's
Note: non-`usr/*` paths are rejected with `InvalidArgs` -- system
conditions belong to Finit's state machine.
A service consuming such a condition:
service alarm {
description = "Foo alarm"
runlevel = "2345"
conditions = { "usr/data-ready" }
command = "alarm --arg foo"
}
asserted from any bus client with `Cond1.Set string:"data-ready"`, see
the examples below.
### Signals
| Signal | Body | Fires when |
@@ -250,12 +286,12 @@ the following subcommands use the bus first:
| `initctl restart` | `Manager1.Restart(svc)` |
| `initctl reload` | `Manager1.Reload()` |
| `initctl reload S` | `Service1.Reload()` (per-svc) |
| `initctl reboot` | `Manager1.Reboot()` |
| `initctl halt` | `Manager1.Halt()` |
| `initctl poweroff` | `Manager1.Poweroff()` |
| `initctl reboot` | `Manager1.Reboot(timeout)` |
| `initctl halt` | `Manager1.Halt(timeout)` |
| `initctl poweroff` | `Manager1.Poweroff(timeout)` |
| `initctl suspend` | `Manager1.Suspend()` |
| `initctl debug` | `Manager1.SetDebug()` |
| `initctl signal` | `Manager1.Signal(svc, signo)` |
| `initctl signal` (or `kill`) | `Manager1.Signal(svc, signo)` |
| `initctl runlevel` | `Properties.Get(Manager1.Runlevel/PrevRunlevel)` |
| `initctl cond set/get/clear` | `Cond1.{Set,Get,Clear}`, `clr` is an alias |
+4
View File
@@ -275,6 +275,10 @@ initctl top # Interactive resource monitor
initctl cond set usr/custom # Set custom condition
```
`initctl` rides Finit's built-in [D-Bus API](dbus.md) when available,
which is also open to any other bus client, including `dbus-send` and
language bindings.
See the [Commands & Status](initctl.md) section for complete documentation.
+1
View File
@@ -32,6 +32,7 @@ Features
* [Pre/Post script actions](config/service-wrappers.md)
* Rudimentary [templating support](config/templating.md)
* [Tooling](initctl.md) to enable/disable services
* [D-Bus API](dbus.md), `org.finit` control and signals for bus clients
* [Automatic reload](features.md#automatic-reload) of modified configuration files (optional)
* [Built-in getty](config/tty.md)
* Built-in watchdog, with support for hand-over to [watchdogd][]
+11 -10
View File
@@ -5,6 +5,11 @@ Finit also implements a modern API to query status, and start/stop
services, called `initctl`. Unlike `telinit` the `initctl` tool does
not return until the given command has fully completed.
Since Finit v5, `initctl` talks to Finit over the built-in
[D-Bus API](dbus.md) when the bus socket is available, falling back
to the legacy `INIT_SOCKET` transport. The `monitor` command streams
the `org.finit` signals until interrupted.
```
Usage: initctl [OPTIONS] [COMMAND]
@@ -14,6 +19,7 @@ Options:
-f, --force Ignore missing files and arguments, never prompt
-h, --help This help text
-j, --json JSON output in 'status' and 'cond' commands
-n, --noerr Ignore error, e.g., already started/enabled/...
-1, --once Only one lap in commands like 'top'
-p, --plain Use plain table headings, no ctrl chars
-q, --quiet Silent, only return status of command
@@ -26,15 +32,8 @@ Commands:
help This help text
version Show program version
ls | list List all .conf in /etc/finit.d
create <CONF> Create .conf in /etc/finit.d/available
delete <CONF> Delete .conf in /etc/finit.d/available
show <CONF> Show .conf in /etc/finit.d/available
edit <CONF> Edit .conf in /etc/finit.d/available
touch <CONF> Change .conf in /etc/finit.d/available
enable <CONF> Enable .conf in /etc/finit.d/available
disable <CONF> Disable .conf in /etc/finit.d/enabled
reload Reload *.conf in /etc/finit.d (activate changes)
show Show /etc/finit.conf
reload Reload /etc/finit.conf (activate changes)
cond set <COND> Set (assert) user-defined conditions +usr/COND
cond get <COND> Get status of user-defined condition, see $? and -v
@@ -50,6 +49,7 @@ Commands:
Note: Finit .conf file(s) are *not* reloaded!
restart <NAME>[:ID] Restart (stop/start) service by name
kill <NAME>[:ID] <S> Send signal S to service by name, with optional ID
monitor Stream D-Bus signals (service state, conditions) until ^C
ident [NAME] Show matching identities for NAME, or all
status <NAME>[:ID] Show service status, by name
status Show status of services, default command
@@ -65,9 +65,10 @@ Commands:
halt Halt system
poweroff Halt and power off system
suspend Suspend system
switch-root NEWROOT [INIT] Switch to new root filesystem (initramfs only)
switch-root ROOT [INIT] Switch to new root filesystem (initramfs)
utmp show Raw dump of UTMP/WTMP db
```
For services *not* supporting `SIGHUP` the `<!>` notation in the .conf
+424 -11
View File
@@ -1,14 +1,427 @@
keventd
=======
Bundled Device Manager
======================
The kernel event daemon bundled with Finit is a simple uevent monitor
for `/sys/class/power_supply`. It provides the `sys/pwr/ac` condition,
which can be useful to prevent power hungry services like anacron to run
when a laptop is only running on battery, for instance.
The kernel event daemon `keventd` is a built-in device manager bundled
with Finit. It replaces the need for external device managers like
mdev, mdevd, or udevd on systems where a lighter-weight solution is
preferred, particularly on embedded systems.
Since keventd is not an integral part of Finit yet it is not enabled by
default. Enable it using `./configure --with-keventd`. The bundled
`contrib/` build scripts for Debian, Alpine, and Void have this enabled.
It is enabled by default since Finit v5, and needs libblkid from
util-linux (the `libblkid-dev` package, or whatever your distribution
calls it) to read the filesystem UUID and label behind the
`/dev/disk/by-uuid/` and `/dev/disk/by-label/` symlinks. To disable it
and use an external device manager instead:
This daemon is planned to be extended with monitoring of other uevents,
patches and ideas are welcome in the issue tracker.
./configure --without-keventd
Features
--------
When started, keventd listens on a `NETLINK_KOBJECT_UEVENT` socket for
kernel events and handles:
- **Device node creation**: creates and removes `/dev` nodes with
correct permissions on device add/remove events
- **udev rules**: matches events against rules read from
`/lib/udev/rules.d/`, `/run/udev/rules.d/`, and `/etc/udev/rules.d/`,
with a curated ruleset derived from eudev installed by default
- **Persistent symlinks**: creates `by-id` and `by-path` symlinks under
`/dev/disk/`, `/dev/input/`, and elsewhere, for stable device naming
- **Firmware loading**: responds to kernel firmware requests by searching
`/lib/firmware/` and writing firmware data to sysfs
- **Module loading**: parses `MODALIAS` from uevents and spawns `modprobe`
to load the appropriate kernel module
- **Coldplug**: with the `-c` flag, walks `/sys/devices` and triggers
add events for all devices present at boot
- **Power supply monitoring**: tracks AC power status and provides the
`sys/pwr/ac` condition
- **Device conditions**: sets `dev/*` conditions in the Finit condition
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
------------
On receiving an `add` event with `MAJOR`, `MINOR`, and `DEVNAME`
fields, keventd creates the corresponding device node in `/dev` using
`mknod()`. Parent directories are created automatically (e.g.,
`/dev/input/` for `/dev/input/event0`).
On `remove` events, the device node and its associated symlinks and
conditions are cleaned up.
### Default Permissions
keventd applies permissions based on built-in rules that match on
device subsystem and name:
| **Subsystem** | **Pattern** | **Mode** | **Owner:Group** |
|---------------|--------------------------------------------|----------|-----------------|
| block | sd*, vd*, nvme*, mmcblk*, loop*, dm-*, md* | 0660 | root:disk |
| tty | tty[0-9]* | 0620 | root:tty |
| tty | ttyS*, ttyUSB*, ttyACM* | 0660 | root:dialout |
| input | event*, mouse*, mice | 0660 | root:root |
| sound | * | 0660 | root:audio |
| video4linux | * | 0660 | root:video |
| drm | card*, render* | 0660 | root:video |
| (any) | null, zero, full, random, urandom | 0666 | root:root |
| (any) | console | 0600 | root:root |
| (default) | | 0660 | root:root |
Persistent Symlinks
-------------------
Symlinks come from two places. A handful are built into keventd and are
created for every device in the subsystem, whatever rules are loaded.
For block devices, under `/dev/disk/`:
- **by-id**: based on the device serial number and model, read from
sysfs attributes (`/sys/.../device/vendor`, `model`, `serial`)
- **by-path**: based on the device topology path
For input devices, under `/dev/input/`:
- **by-id**: based on the device name from sysfs
- **by-path**: based on the physical device path
The rest come from `SYMLINK+=` in the udev rules, so what you get
depends on the ruleset installed. The curated rules Finit ships add,
among others:
| **Directory** | **Links** |
|----------------|--------------------------------------------------------|
| `/dev/disk/` | `by-uuid`, `by-label`, `by-partuuid`, `by-partlabel`, `by-diskseq`, and further `by-id` and `by-path` names for NVMe, virtio, MMC, WWN, and FireWire |
| `/dev/input/` | longer `by-id` and `by-path` names than the built-in ones, down to the USB interface number |
| `/dev/serial/` | `by-id`, `by-path` |
| `/dev/snd/` | `by-id`, `by-path` |
| `/dev/v4l/` | `by-id`, `by-path` |
| `/dev/tape/` | `by-id`, `by-path` |
| `/dev/dri/` | `by-path` |
They also set up a few fixed names: `/dev/rtc`, `/dev/cdrom`, and
`/dev/virtio-ports/<name>`.
The `by-uuid` and `by-label` links are why keventd needs libblkid. The
rules read the filesystem metadata off the device with
`IMPORT{builtin}="blkid"`, and libblkid is what that builtin calls.
Built-in and rule-provided symlinks alike are tracked internally and
removed when the corresponding device is unplugged.
Firmware Loading
----------------
When a kernel driver requests firmware (via `request_firmware()`), the
kernel sends a uevent with a `FIRMWARE=` field. keventd handles this
by:
1. Searching for the firmware file in order:
- `/lib/firmware/updates/<kernel-version>/<name>`
- `/lib/firmware/updates/<name>`
- `/lib/firmware/<kernel-version>/<name>`
- `/lib/firmware/<name>`
2. Writing `1` to `/sys/<devpath>/loading` to signal start
3. Copying the firmware data to `/sys/<devpath>/data`
4. Writing `0` to `/sys/<devpath>/loading` on success (or `-1` on failure)
This is particularly important early in boot when drivers for graphics
cards, network adapters, and other hardware need firmware before they
can operate.
Module Loading
--------------
When a device add event includes a `MODALIAS` field, keventd spawns
`modprobe -bq <modalias>` to load the matching kernel module. Module
loading is done asynchronously (keventd does not wait for modprobe to
complete) to avoid blocking other event processing.
Coldplug
--------
To handle devices that were present before keventd started, it supports
a coldplug mode activated with the `-c` flag. This walks the entire
`/sys/devices` tree and writes `add` to each `uevent` file, causing the
kernel to re-emit add events for all existing devices.
This replaces the separate `coldplug` script previously used with mdev.
When `-c` is used, keventd defers its pidfile (and the `pid/keventd`
condition Finit asserts from it) until the coldplug event queue has
been fully drained. Services that depend on `<pid/keventd>` can
therefore assume `/dev` is populated and persistent symlinks are live,
without needing a separate `settle` step.
Netlink Rebroadcast
-------------------
The Linux kernel sends uevents to netlink multicast group 1 (bit 0) of
`NETLINK_KOBJECT_UEVENT`. Only the device manager listens on this raw
kernel group. Userspace consumers — applications using libudev —
expect to receive processed events on a separate netlink group.
systemd/udevd established the convention of rebroadcasting processed
events to a separate group, and [libudev-zero][], a daemonless drop-in
replacement for libudev, listens on group `0x4` for these events.
Without a device manager rebroadcasting, graphical applications,
Wayland/X11 compositors, libinput, and anything else using libudev to
monitor device hotplug will never see any events.
keventd rebroadcasts by default to netlink group 4 (`0x4`). A second
netlink socket is created at startup, and after each uevent has been
fully processed (device nodes created, symlinks set up, modules loaded),
the original raw event is sent to the configured group(s).
Rebroadcasting after processing ensures that device nodes and symlinks
already exist by the time consumers receive the notification.
Use `-g GROUP` to override the default group mask, or `-G` to disable
rebroadcast entirely. Bit 0 of the group mask is always forced off to
prevent a feedback loop with the kernel's own multicast group.
### Background
The netlink uevent architecture uses separate multicast groups to
isolate the kernel-to-device-manager channel from the device-manager-to-
application channel:
| **Group** | **Bit** | **Purpose** |
|-----------|---------|---------------------------------------------|
| 1 | 0 | Kernel events (device manager listens here) |
| 4 | 2 | Processed events (libudev consumers listen) |
This two-group design was established by systemd/udevd and is the de
facto standard. [mdevd][] implements the same mechanism via its `-O`
flag, and keventd follows the same convention.
For more details, see:
- [libudev-zero][] — daemonless replacement for libudev
- [mdevd][] — mdev-compatible device manager with rebroadcast support
Conditions
----------
keventd provides conditions in two namespaces:
### Device Conditions (`dev/`)
When a device node is created, keventd asserts a corresponding condition
in `/run/finit/cond/dev/`. This allows services to wait for specific
devices:
service mdadm {
description = "RAID monitor"
runlevel = "2345"
conditions = { "dev/sda" }
command = "/usr/sbin/mdadm --monitor /dev/md0"
}
service gps-daemon {
description = "GPS daemon"
runlevel = "2345"
conditions = { "dev/ttyUSB0" }
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
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/`)
keventd monitors the `power_supply` subsystem and provides:
- `sys/pwr/ac` — asserted when AC power is connected
This is useful for preventing power-hungry services from running on
battery:
service cron {
description = "Cron daemon"
runlevel = "2345"
conditions = { "sys/pwr/ac", "pid/syslogd" }
command = "cron -f"
}
Usage
-----
keventd [-cdGhnpSv] [-g GROUP] [-r DIR] [-t SECONDS]
Options:
-c Run coldplug at startup
-d Enable debug mode (foreground, verbose)
-g GROUP Override netlink rebroadcast group (default: 4)
-G Disable netlink rebroadcast entirely
-h Show help text
-n Run in foreground (no daemon)
-p Passive mode: power supply events only
-r DIR Extra rules directory
-S Settle mode: wait for kernel uevent queue to quiet, then exit
-t SEC Settle timeout (default 30s, used with -S)
-v Show version
In normal operation, Finit starts keventd automatically via its system
configuration. The `-d` flag is useful for debugging device issues --
it runs keventd in the foreground and logs all received uevents.
`keventd -S` is a one-shot command, not a flag to the running daemon.
It is the `udevadm settle` equivalent for migration scenarios:
keventd -S -t 10 && start-graphical-session
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 (`<dev/X>`, `<class/...>`,
`<driver/...>`) over settle when you control the service definition --
settle is racy with slow probes that fire after the queue appears
quiet. It is provided for legacy boot scripts and init transitions
where condition wiring isn't feasible.
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
----------------------
keventd is a standalone daemon started by Finit as an internal service.
It communicates with Finit exclusively through the filesystem-based
condition system — creating and removing symlinks in `/run/finit/cond/`.
This means keventd can also be tested independently:
# Run in debug mode to see all kernel events
keventd -d
# Run with coldplug to populate /dev from scratch
keventd -c -n
# Run without rebroadcast (e.g., headless embedded system)
keventd -c -G
Only one device manager should be active at a time. This is settled
at build time: with the hotplug plugin enabled, Finit starts keventd
in passive mode (`-p`), leaving device management to the udevd, mdevd,
or mdev service from `system/10-hotplug.conf` and monitoring only
power supply events. Without the hotplug plugin, keventd runs as the
system device manager (`keventd -c`).
[libudev-zero]: https://github.com/illiliti/libudev-zero
[mdevd]: https://skarnet.org/software/mdevd/
+5
View File
@@ -40,6 +40,11 @@ For your convenience a set of *optional* plugins are available:
`tmpfiles.d/dbus.conf`, both overridable from `/etc`; the plugin
itself only handles what has to inspect the running system.
Not to be confused with Finit's built-in [D-Bus API](dbus.md): the
plugin (`--disable-dbus-plugin`) starts the external system bus
daemon, while the built-in support (`--disable-dbus`) is the
`org.finit` interface served by Finit itself.
* *hook-scripts.so*: Trigger the execution of scripts from plugin hook
points (see [Hooks](#hooks)). _Optional plugin._
+123
View File
@@ -0,0 +1,123 @@
# How udev rules work
## Summary of the complete fix
- hardware.c: Write authorized_default=1 on the bus path before nftw
runs (just 2 lines added). This exploits the fact that apath already
contains path/authorized_default at that point (set by the snprintf on
line 71). With authorized_default=1 set on the root hub, any device
that appears behind an intermediate hub during hub enumeration gets
auto-authorized by the kernel.
- 70-usb-authorize.rules: Belt-and-suspenders for the async case. If hub
port probing is slow enough that children appear after nftw has
already returned, udev sees the add event with authorized=0, walks up
the parent chain with ATTRS{authorized_default}=="1", and authorizes
the device. Locked buses (authorized_default=0) are correctly skipped.
```
ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", \
ATTR{authorized}=="0", \
ATTRS{authorized_default}=="1", \
RUN+="/bin/sh -c 'echo 1 > /sys%p/authorized'"
```
This keeps authorized_default=2 (deny by default) fully intact — the
security window stays closed. The udev rule only fires when confd has
explicitly marked a bus as unlocked.
## Comment from Undersigned
Very interesting, I did not know you could do such complex operations
with udev rules. Could you explain a bit more in detail how the matching
works and how we can guarantee locked buses/ports are not accidentally unlocked?
> Sure. Let's walk through it step by step.
## How udev ATTRS{} matching works
A udev rule matches against a device and its entire parent chain in the
sysfs device tree. When a device like 2-1.4 (the keyboard) is added, its
sysfs hierarchy looks like:
```
/sys/devices/.../0000:01:00.0/usb2/ ← root hub (usbN)
usb2/2-1/ ← VIA Labs hub
usb2/2-1/2-1.4/ ← keyboard ← event fires here
```
- ATTR{} (no trailing S) — matches attributes on the device itself only
- ATTRS{} (with S) — matches attributes on the device or any of its
parents, walking up the tree until it finds a match or runs out of
parents
So ATTRS{authorized_default}=="1" on a keyboard event at 2-1.4 walks:
1. 2-1.4/authorized_default — doesn't exist (not a hub/controller)
2. 2-1/authorized_default — doesn't exist (downstream hubs don't have
this attribute, only root hubs do)
3. usb2/authorized_default — exists, check its value
This is exactly the right level: authorized_default only lives on root
hubs (usbN), which is precisely where confd writes.
## Why locked buses can't be accidentally unlocked
The invariant is: confd owns `authorized_default`. It sets it to 0
(locked) or 1 (unlocked). The udev rule only reads it, never writes it.
Walk through the locked case:
```
confd sets usb2/authorized_default = 0 ← locked
```
A device plugs into usb2. udev fires:
1. ATTR{authorized}=="0" — matches (device is unauthorized)
2. ATTRS{authorized_default}=="1" — walks up to usb2, finds 0 → no match
3. Rule doesn't fire. Device stays denied. ✓
For a device behind an intermediate hub on a locked bus:
```
usb2/authorized_default = 0 ← bus locked
usb2/2-1/ ← hub (somehow authorized)
usb2/2-1/2-1.4/ ← keyboard
```
Even if the hub itself were authorized somehow, the keyboard's parent
chain still reaches `usb2/authorized_default = 0` before finding any
`authorized_default = 1`. The rule doesn't fire. ✓
The only remaining question: what if two buses exist and one is locked?
Say usb1 is unlocked and usb2 is locked. A device on usb2 fires a udev
event. ATTRS walks up: `usb2/authorized_default = 0` — stops there (once
it finds the attribute it stops). It does not continue up to PCIe or
platform buses looking for usb1's attribute. Each USB device's parent
chain only goes through its own root hub.
This is because authorized_default is found at usb2 — udev stops as soon
as it matches the attribute name, regardless of value. If the value
doesn't match the condition, the whole rule fails. It doesn't continue
looking at other buses.
## Summary of the guarantee
```
┌───────────────────────┬─────────────────────────┬─────────────────────────┐
│ Scenario │ usb2/authorized_default │ Rule fires? │
├───────────────────────┼─────────────────────────┼─────────────────────────┤
│ Bus unlocked by confd │ 1 │ Yes → device authorized │
├───────────────────────┼─────────────────────────┼─────────────────────────┤
│ Bus locked by confd │ 0 │ No → device denied │
├───────────────────────┼─────────────────────────┼─────────────────────────┤
│ Bus at kernel default │ 2 │ No → device denied │
├───────────────────────┼─────────────────────────┼─────────────────────────┤
│ Bus doesn't exist yet │ — │ No → no match │
└───────────────────────┴─────────────────────────┴─────────────────────────┘
```
The rule is purely reactive: it can only authorize devices on buses that
confd has already explicitly declared unlocked. It has no ability to
override a lock.
+4
View File
@@ -0,0 +1,4 @@
.deps/
keventd
Makefile
Makefile.in
+67
View File
@@ -0,0 +1,67 @@
AM_CPPFLAGS = -D_XOPEN_SOURCE=600 -D_BSD_SOURCE -D_GNU_SOURCE -D_DEFAULT_SOURCE
AM_CPPFLAGS += -I$(top_srcdir)/src
pkglibexec_PROGRAMS = keventd
keventd_SOURCES = keventd.c keventd.h uevent.c \
udevdb.c udevdb.h \
sysfs.c sysfs.h \
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
# handled via explicit checks where present. Matches systemd-udev policy.
keventd_CFLAGS += -Wno-format-truncation
keventd_CFLAGS += $(lite_CFLAGS)
keventd_CFLAGS += $(uev_CFLAGS)
keventd_CFLAGS += $(blkid_CFLAGS)
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
# precedence. See .notes/curated-rules-manifest.md.
if UDEV_RULES
udevrulesdir = $(prefix)/lib/udev/rules.d
dist_udevrules_DATA = \
rules.d/50-udev-default.rules \
rules.d/60-autosuspend.rules \
rules.d/60-block.rules \
rules.d/60-cdrom_id.rules \
rules.d/60-drm.rules \
rules.d/60-evdev.rules \
rules.d/60-fido-id.rules \
rules.d/60-input-id.rules \
rules.d/60-persistent-alsa.rules \
rules.d/60-persistent-input.rules \
rules.d/60-persistent-storage.rules \
rules.d/60-persistent-storage-tape.rules \
rules.d/60-persistent-v4l.rules \
rules.d/60-sensor.rules \
rules.d/60-serial.rules \
rules.d/64-btrfs.rules \
rules.d/70-camera.rules \
rules.d/70-joystick.rules \
rules.d/70-memory.rules \
rules.d/70-mouse.rules \
rules.d/70-touchpad.rules \
rules.d/75-net-description.rules \
rules.d/75-probe_mtd.rules \
rules.d/78-sound-card.rules \
rules.d/80-drivers.rules \
rules.d/80-net-name-slot.rules \
rules.d/81-net-dhcp.rules
endif
+1529
View File
File diff suppressed because it is too large. Load diff
+46
View File
@@ -0,0 +1,46 @@
/* Builtin command dispatch for keventd rules engine
*
* Copyright (c) 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.
*/
#ifndef KEVENTD_BUILTIN_H_
#define KEVENTD_BUILTIN_H_
#include "keventd.h"
/*
* Execute a builtin command from a udev rule.
* cmd is the full command string, e.g. "hwdb --subsystem=input" or "path_id".
* Returns 0 on success, -1 if the builtin is unknown or fails.
*/
int builtin_run(struct uevent *ev, const char *cmd);
/* True if NAME is a registered builtin (e.g. "path_id", "usb_id"). */
int builtin_has(const char *name);
#endif /* KEVENTD_BUILTIN_H_ */
/**
* Local Variables:
* indent-tabs-mode: t
* c-file-style: "linux"
* End:
*/
+577
View File
@@ -0,0 +1,577 @@
/* 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 <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"
#ifdef HAVE_DBUS
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>
#include <uev/uev.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
# include <lite/lite.h>
#endif
#include "link.h"
#include "keventd.h"
#include "udevdb.h"
#include "util.h"
#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;
/*
* 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 = kev_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 = kev_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 = kev_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 */
+851
View File
@@ -0,0 +1,851 @@
/* 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 (in addition to 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:
*/
+191
View File
@@ -0,0 +1,191 @@
/* Unified device manager for Finit - uevent handling, device nodes, symlinks
*
* 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.
*/
#ifndef FINIT_KEVENTD_H_
#define FINIT_KEVENTD_H_
#ifdef _LIBITE_LITE
# include <libite/queue.h> /* BSD sys/queue.h API */
#else
# include <lite/queue.h> /* BSD sys/queue.h API */
#endif
#include <stdint.h>
#include <stdio.h>
#include <sys/types.h>
/* Maximum uevent buffer size (kernel uses 8192 internally) */
#define UEVENT_BUFFER_SIZE 8192
/* Capacity of the generic env store on struct uevent */
#define UEVENT_ENV_MAX 64
/* Maximum TAG+= entries per device event */
#define UEVENT_TAG_MAX 16
/* Maximum SYMLINK+= and RUN+= entries set by the rules engine */
#define RULE_SYMLINK_MAX 8
#define RULE_RUN_MAX 8
/* RUN{type} subtypes — also used in struct rule_ctx below */
typedef enum {
RUN_PROGRAM = 0, /* execute external program */
RUN_BUILTIN, /* call builtin function */
} run_type_t;
/* Uevent actions from kernel */
typedef enum {
ACT_UNKNOWN = 0,
ACT_ADD,
ACT_REMOVE,
ACT_CHANGE,
ACT_MOVE,
ACT_ONLINE,
ACT_OFFLINE,
ACT_BIND,
ACT_UNBIND,
} uevent_action_t;
/*
* Rule-applied permissions and context, set by the rules engine.
* Zero-initialised means "nothing set by rules", fall back to built-in table.
*/
struct rule_ctx {
/* Device permissions */
mode_t mode;
uid_t uid;
gid_t gid;
int has_mode;
int has_owner;
int has_group;
int final_mode; /* := operator locks against further overrides */
int final_owner;
int final_group;
/* NAME= override for device node path (heap-allocated) */
char *name;
/* SYMLINK+= accumulator (heap-allocated strings) */
int nsymlinks;
char *symlinks[RULE_SYMLINK_MAX];
/* RUN+= command list (heap-allocated strings, executed post-event) */
int nruncmds;
char *run_cmds[RULE_RUN_MAX];
run_type_t run_types[RULE_RUN_MAX];
};
/*
* Parsed uevent structure.
* Named field pointers point into the receive buffer (zero-copy).
* The env store holds all KEY=VALUE pairs plus any rule-set variables.
*/
struct uevent {
uevent_action_t action;
char *devpath; /* /devices/pci0000:00/... */
char *subsystem; /* block, input, net, power_supply */
char *devname; /* sda, event0, ttyUSB0 */
char *devtype; /* disk, partition */
int major;
int minor;
char *modalias; /* module alias for auto-loading */
char *firmware; /* firmware file name request */
char *seqnum; /* kernel sequence number */
char *driver; /* driver name */
/* Generic env store: all KEY=VALUE from the netlink buffer + rule-set vars */
int nenv;
char *env_key[UEVENT_ENV_MAX];
char *env_val[UEVENT_ENV_MAX];
uint8_t env_key_alloc[UEVENT_ENV_MAX]; /* 1 if heap-allocated, 0 if buf ptr */
uint8_t env_val_alloc[UEVENT_ENV_MAX];
/* PROGRAM= output, stored for subsequent RESULT== matching */
char *result;
/* TAG+= accumulator */
int ntags;
char *tags[UEVENT_TAG_MAX];
/* Rule-applied permissions from the rules engine */
struct rule_ctx applied;
};
/* Tracked symlink for cleanup on device removal */
struct dev_symlink {
TAILQ_ENTRY(dev_symlink) link;
char *devpath; /* sysfs devpath (key for removal) */
char *linkpath; /* /dev/disk/by-id/... */
};
/* Function prototypes - uevent.c */
int uevent_parse (char *buf, size_t len, struct uevent *ev);
const char *uevent_action_str(uevent_action_t action);
const char *uevent_sysname (const struct uevent *ev);
const char *uevent_getenv (const struct uevent *ev, const char *key);
int uevent_setenv (struct uevent *ev, const char *key, const char *val);
void uevent_env_free (struct uevent *ev);
int devnode_add (struct uevent *ev);
int devnode_del (struct uevent *ev);
int netdev_add (struct uevent *ev);
void cond_emit (const char *prefix, const char *rel, int set);
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);
int firmware_load (struct uevent *ev);
int modprobe_load (const char *modalias);
int coldplug (void);
int coldplug_trigger (const char *action, const char *subsys_glob);
int uevent_action_valid(const char *action);
/* keventd.c: logging and queue/rules state, for all of keventd */
void logit (int prio, const char *fmt, ...)
__attribute__((format(printf, 2, 3)));
uint64_t kev_now_ms (void);
unsigned long long kev_seq_processed(void);
int kev_queue_empty (void);
int kev_rules_reload (void);
#ifdef HAVE_DBUS
#include <uev/uev.h>
/* 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_ */
/**
* Local Variables:
* indent-tabs-mode: t
* c-file-style: "linux"
* End:
*/
+1626
View File
File diff suppressed because it is too large. Load diff
+121
View File
@@ -0,0 +1,121 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/50-udev-default.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
# run a command on remove events
ACTION=="remove", ENV{REMOVE_CMD}!="", RUN+="$env{REMOVE_CMD}"
ACTION=="remove", GOTO="default_end"
SUBSYSTEM=="virtio-ports", KERNEL=="vport*", ATTR{name}=="?*", SYMLINK+="virtio-ports/$attr{name}"
# select "system RTC" or just use the first one
SUBSYSTEM=="rtc", ATTR{hctosys}=="1", SYMLINK+="rtc"
SUBSYSTEM=="rtc", KERNEL=="rtc0", SYMLINK+="rtc", OPTIONS+="link_priority=-100"
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", IMPORT{builtin}="usb_id", IMPORT{builtin}="hwdb --subsystem=usb"
ENV{MODALIAS}!="", IMPORT{builtin}="hwdb --subsystem=$env{SUBSYSTEM}"
ACTION!="add", GOTO="default_end"
SUBSYSTEM=="tty", KERNEL=="ptmx", GROUP="tty", MODE="0666"
SUBSYSTEM=="tty", KERNEL=="tty", GROUP="tty", MODE="0666"
SUBSYSTEM=="tty", KERNEL=="tty[0-9]*", GROUP="tty", MODE="0620"
SUBSYSTEM=="tty", KERNEL=="sclp_line[0-9]*", GROUP="tty", MODE="0620"
SUBSYSTEM=="tty", KERNEL=="ttysclp[0-9]*", GROUP="tty", MODE="0620"
SUBSYSTEM=="tty", KERNEL=="3270/tty[0-9]*", GROUP="tty", MODE="0620"
SUBSYSTEM=="vc", KERNEL=="vcs*|vcsa*", GROUP="tty"
KERNEL=="tty[A-Z]*[0-9]|ttymxc[0-9]*|pppox[0-9]*|ircomm[0-9]*|noz[0-9]*|rfcomm[0-9]*", GROUP="dialout"
SUBSYSTEM=="mem", KERNEL=="mem|kmem|port", GROUP="kmem", MODE="0640"
SUBSYSTEM=="input", GROUP="input"
SUBSYSTEM=="input", KERNEL=="js[0-9]*", MODE="0664"
SUBSYSTEM=="video4linux", GROUP="video"
SUBSYSTEM=="graphics", GROUP="video"
SUBSYSTEM=="drm", KERNEL!="renderD*", GROUP="video"
SUBSYSTEM=="dvb", GROUP="video"
SUBSYSTEM=="media", GROUP="video"
SUBSYSTEM=="cec", GROUP="video"
SUBSYSTEM=="drm", KERNEL=="renderD*", GROUP="video", MODE="0666"
SUBSYSTEM=="kfd", GROUP="video", MODE="0666"
SUBSYSTEM=="misc", KERNEL=="sgx_enclave", GROUP="sgx", MODE="0660"
SUBSYSTEM=="misc", KERNEL=="sgx_vepc", GROUP="sgx", MODE="0660"
# When using static_node= with non-default permissions, also update
# tmpfiles.d/static-nodes-permissions.conf.in to keep permissions synchronized.
SUBSYSTEM=="sound", GROUP="audio", \
OPTIONS+="static_node=snd/seq", OPTIONS+="static_node=snd/timer"
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0664"
SUBSYSTEM=="firewire", TEST=="units", TEST=="model", \
IMPORT{builtin}="hwdb 'ieee1394:node:ven$attr{vendor}mo$attr{model}units$attr{units}'"
SUBSYSTEM=="firewire", TEST=="units", TEST!="model", \
IMPORT{builtin}="hwdb 'ieee1394:node:ven$attr{vendor}units$attr{units}'"
SUBSYSTEM=="firewire", TEST=="units", ENV{IEEE1394_UNIT_FUNCTION_MIDI}=="1", GROUP="audio"
SUBSYSTEM=="firewire", TEST=="units", ENV{IEEE1394_UNIT_FUNCTION_AUDIO}=="1", GROUP="audio"
SUBSYSTEM=="firewire", TEST=="units", ENV{IEEE1394_UNIT_FUNCTION_VIDEO}=="1", GROUP="video"
KERNEL=="parport[0-9]*", GROUP="lp"
SUBSYSTEM=="printer", KERNEL=="lp*", GROUP="lp"
SUBSYSTEM=="ppdev", GROUP="lp"
KERNEL=="lp[0-9]*", GROUP="lp"
KERNEL=="irlpt[0-9]*", GROUP="lp"
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ENV{ID_USB_INTERFACES}=="*:0701??:*", GROUP="lp"
SUBSYSTEM=="block", GROUP="disk"
SUBSYSTEM=="block", KERNEL=="sr[0-9]*", GROUP="cdrom"
SUBSYSTEM=="scsi_generic", SUBSYSTEMS=="scsi", ATTRS{type}=="4|5", GROUP="cdrom"
KERNEL=="sch[0-9]*", GROUP="cdrom"
KERNEL=="pktcdvd[0-9]*", GROUP="cdrom"
KERNEL=="pktcdvd", GROUP="cdrom"
SUBSYSTEM=="scsi_generic|scsi_tape", SUBSYSTEMS=="scsi", ATTRS{type}=="1|8", GROUP="tape"
SUBSYSTEM=="scsi_generic", SUBSYSTEMS=="scsi", ATTRS{type}=="0", GROUP="disk"
KERNEL=="qft[0-9]*|nqft[0-9]*|zqft[0-9]*|nzqft[0-9]*|rawqft[0-9]*|nrawqft[0-9]*", GROUP="disk"
KERNEL=="loop-control", GROUP="disk", OPTIONS+="static_node=loop-control"
KERNEL=="btrfs-control", GROUP="disk"
KERNEL=="rawctl", GROUP="disk"
SUBSYSTEM=="raw", KERNEL=="raw[0-9]*", GROUP="disk"
SUBSYSTEM=="aoe", GROUP="disk", MODE="0220"
SUBSYSTEM=="aoe", KERNEL=="err", MODE="0440"
KERNEL=="rfkill", MODE="0664"
KERNEL=="tun", MODE="0666", OPTIONS+="static_node=net/tun"
KERNEL=="fuse", MODE="0666", OPTIONS+="static_node=fuse"
# The static_node is required on s390x and ppc (they are using MODULE_ALIAS)
KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"
KERNEL=="vfio", MODE="0666", OPTIONS+="static_node=vfio/vfio"
KERNEL=="vsock", MODE="0666"
KERNEL=="vhost-vsock", GROUP="kvm", MODE="0666", OPTIONS+="static_node=vhost-vsock"
KERNEL=="vhost-net", GROUP="kvm", MODE="0666", OPTIONS+="static_node=vhost-net"
KERNEL=="udmabuf", GROUP="kvm"
SUBSYSTEM=="ptp", ATTR{clock_name}=="KVM virtual PTP", SYMLINK += "ptp_kvm"
SUBSYSTEM=="ptp", ATTR{clock_name}=="hyperv", SYMLINK += "ptp_hyperv"
SUBSYSTEM!="dmi", GOTO="dmi_end"
ENV{ID_VENDOR}="$attr{sys_vendor}"
ENV{ID_SYSFS_ATTRIBUTE_MODEL}=="|product_name", ENV{ID_MODEL}="$attr{product_name}"
ENV{ID_SYSFS_ATTRIBUTE_MODEL}=="product_version", ENV{ID_MODEL}="$attr{product_version}"
# fallback to board information
ENV{ID_VENDOR}=="", ENV{ID_VENDOR}="$attr{board_vendor}"
ENV{ID_MODEL}=="", ENV{ID_MODEL}="$attr{board_name}"
LABEL="dmi_end"
LABEL="default_end"
+22
View File
@@ -0,0 +1,22 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/60-autosuspend.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION!="add", GOTO="autosuspend_end"
# I2C rules
SUBSYSTEM=="i2c", ATTR{name}=="cyapa", \
ATTR{power/control}="on", GOTO="autosuspend_end"
# Enable autosuspend if hwdb says so. Here we are relying on
# the hwdb import done earlier based on MODALIAS.
ENV{ID_AUTOSUSPEND}=="1", TEST=="power/control", \
ATTR{power/control}="auto"
# Disable USB persist if hwdb says so.
ENV{ID_PERSIST}=="0", TEST=="power/persist", \
ATTR{power/persist}="0"
LABEL="autosuspend_end"
+17
View File
@@ -0,0 +1,17 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/60-block.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
# enable in-kernel media-presence polling
ACTION=="add", SUBSYSTEM=="module", KERNEL=="block", ATTR{parameters/events_dfl_poll_msecs}=="0", \
ATTR{parameters/events_dfl_poll_msecs}="2000"
# forward scsi device event to corresponding block device
ACTION=="change", SUBSYSTEM=="scsi", ENV{DEVTYPE}=="scsi_device", TEST=="block", ATTR{block/*/uevent}="change"
# watch metadata changes, caused by tools closing the device node which was opened for writing
ACTION!="remove", SUBSYSTEM=="block", \
KERNEL=="loop*|mmcblk*[0-9]|msblk*[0-9]|mspblk*[0-9]|nvme*|sd*|vd*|xvd*|bcache*|cciss*|dasd*|ubd*|ubi*|scm*|pmem*|nbd*|zd*", \
OPTIONS+="watch"
+33
View File
@@ -0,0 +1,33 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/60-cdrom_id.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="cdrom_end"
SUBSYSTEM!="block", GOTO="cdrom_end"
KERNEL!="sr[0-9]*|vdisk*|xvd*", GOTO="cdrom_end"
ENV{DEVTYPE}!="disk", GOTO="cdrom_end"
# unconditionally tag device as CDROM
KERNEL=="sr[0-9]*", ENV{ID_CDROM}="1"
# stop automatically any mount units bound to the device if the media eject
# button is pressed.
ENV{ID_CDROM}=="1", ENV{SYSTEMD_MOUNT_DEVICE_BOUND}="1"
# media eject button pressed
ENV{DISK_EJECT_REQUEST}=="?*", RUN+="cdrom_id --eject-media $devnode", GOTO="cdrom_end"
# import device and media properties and lock tray to
# enable the receiving of media eject button events
IMPORT{program}="cdrom_id --lock-media $devnode"
# ejecting a CD does not remove the device node, so mark the systemd device
# unit as inactive while there is no medium; this automatically cleans up of
# stale mounts after ejecting
ENV{DISK_MEDIA_CHANGE}=="?*", ENV{ID_CDROM_MEDIA}!="?*", ENV{SYSTEMD_READY}="0"
KERNEL=="sr0", SYMLINK+="cdrom", OPTIONS+="link_priority=-100"
LABEL="cdrom_end"
+12
View File
@@ -0,0 +1,12 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/60-drm.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION!="remove", SUBSYSTEM=="drm", SUBSYSTEMS=="pci|usb|platform", IMPORT{builtin}="path_id"
# by-path
ENV{ID_PATH}=="?*", KERNEL=="card*", SYMLINK+="dri/by-path/$env{ID_PATH}-card"
ENV{ID_PATH}=="?*", KERNEL=="controlD*", SYMLINK+="dri/by-path/$env{ID_PATH}-control"
ENV{ID_PATH}=="?*", KERNEL=="renderD*", SYMLINK+="dri/by-path/$env{ID_PATH}-render"
+29
View File
@@ -0,0 +1,29 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/60-evdev.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="evdev_end"
KERNEL!="event*", GOTO="evdev_end"
# skip later rules when we find something for this input device
IMPORT{builtin}="hwdb --subsystem=input --lookup-prefix=evdev:", \
RUN{builtin}+="keyboard", GOTO="evdev_end"
# AT keyboard matching by the machine's DMI data
DRIVERS=="atkbd", \
IMPORT{builtin}="hwdb 'evdev:atkbd:$attr{[dmi/id]modalias}'", \
RUN{builtin}+="keyboard", GOTO="evdev_end"
# device matching the input device name + properties + the machine's DMI data
KERNELS=="input*", \
IMPORT{builtin}="hwdb 'evdev:name:$attr{name}:phys:$attr{phys}:ev:$attr{capabilities/ev}:$attr{[dmi/id]modalias}'", \
RUN{builtin}+="keyboard", GOTO="evdev_end"
# device matching the input device name and the machine's DMI data
KERNELS=="input*", \
IMPORT{builtin}="hwdb 'evdev:name:$attr{name}:$attr{[dmi/id]modalias}'", \
RUN{builtin}+="keyboard", GOTO="evdev_end"
LABEL="evdev_end"
+18
View File
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/60-fido-id.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="fido_id_end"
SUBSYSTEM=="hidraw", IMPORT{program}="fido_id"
# Tag any form of security token as such
ENV{ID_SECURITY_TOKEN}=="1", TAG+="security-device"
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ENV{ID_USB_INTERFACES}=="*:0b????:*", ENV{ID_SMARTCARD_READER}="1"
# Tag any CCID device (i.e. Smartcard Reader) as security token
ENV{ID_SMARTCARD_READER}=="1", TAG+="security-device"
LABEL="fido_id_end"
+12
View File
@@ -0,0 +1,12 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/60-input-id.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="id_input_end"
SUBSYSTEM=="input", ENV{ID_INPUT}=="", IMPORT{builtin}="input_id"
SUBSYSTEM=="input", IMPORT{builtin}="hwdb --subsystem=input --lookup-prefix=id-input:modalias:"
LABEL="id_input_end"
+18
View File
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/60-persistent-alsa.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="persistent_alsa_end"
SUBSYSTEM!="sound", GOTO="persistent_alsa_end"
KERNEL!="controlC[0-9]*", GOTO="persistent_alsa_end"
SUBSYSTEMS=="usb", ENV{ID_MODEL}=="", IMPORT{builtin}="usb_id"
ENV{ID_SERIAL}=="?*", ENV{ID_USB_INTERFACE_NUM}=="?*", SYMLINK+="snd/by-id/$env{ID_BUS}-$env{ID_SERIAL}-$env{ID_USB_INTERFACE_NUM}"
ENV{ID_SERIAL}=="?*", ENV{ID_USB_INTERFACE_NUM}=="", SYMLINK+="snd/by-id/$env{ID_BUS}-$env{ID_SERIAL}"
IMPORT{builtin}="path_id"
ENV{ID_PATH}=="?*", SYMLINK+="snd/by-path/$env{ID_PATH}"
LABEL="persistent_alsa_end"
+46
View File
@@ -0,0 +1,46 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/60-persistent-input.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="persistent_input_end"
SUBSYSTEM!="input", GOTO="persistent_input_end"
SUBSYSTEMS=="bluetooth", ENV{ID_BUS}="bluetooth", GOTO="persistent_input_end"
# Bluetooth devices don't always have the bluetooth subsystem
ATTRS{id/bustype}=="0005", ENV{ID_BUS}="bluetooth", GOTO="persistent_input_end"
SUBSYSTEMS=="rmi4", ENV{ID_BUS}="rmi"
SUBSYSTEMS=="serio", ENV{ID_BUS}="i8042"
SUBSYSTEMS=="usb", ENV{ID_BUS}=="", IMPORT{builtin}="usb_id"
# determine class name for persistent symlinks
ENV{ID_INPUT_KEYBOARD}=="?*", ENV{.INPUT_CLASS}="kbd"
ENV{ID_INPUT_MOUSE}=="?*", ENV{.INPUT_CLASS}="mouse"
ENV{ID_INPUT_TOUCHPAD}=="?*", ENV{.INPUT_CLASS}="mouse"
ENV{ID_INPUT_TABLET}=="?*", ENV{.INPUT_CLASS}="mouse"
ENV{ID_INPUT_JOYSTICK}=="?*", ENV{.INPUT_CLASS}="joystick"
DRIVERS=="pcspkr", ENV{.INPUT_CLASS}="spkr"
ATTRS{name}=="*dvb*|*DVB*|* IR *", ENV{.INPUT_CLASS}="ir"
# fill empty serial number
ENV{.INPUT_CLASS}=="?*", ENV{ID_SERIAL}=="", ENV{ID_SERIAL}="noserial"
# by-id links
KERNEL=="mouse*|js*", ENV{ID_BUS}=="?*", ENV{.INPUT_CLASS}=="?*", ATTRS{bInterfaceNumber}=="|00", SYMLINK+="input/by-id/$env{ID_BUS}-$env{ID_SERIAL}-$env{.INPUT_CLASS}"
KERNEL=="mouse*|js*", ENV{ID_BUS}=="?*", ENV{.INPUT_CLASS}=="?*", ATTRS{bInterfaceNumber}=="?*", ATTRS{bInterfaceNumber}!="00", SYMLINK+="input/by-id/$env{ID_BUS}-$env{ID_SERIAL}-if$attr{bInterfaceNumber}-$env{.INPUT_CLASS}"
KERNEL=="event*", ENV{ID_BUS}=="?*", ENV{.INPUT_CLASS}=="?*", ATTRS{bInterfaceNumber}=="|00", SYMLINK+="input/by-id/$env{ID_BUS}-$env{ID_SERIAL}-event-$env{.INPUT_CLASS}"
KERNEL=="event*", ENV{ID_BUS}=="?*", ENV{.INPUT_CLASS}=="?*", ATTRS{bInterfaceNumber}=="?*", ATTRS{bInterfaceNumber}!="00", SYMLINK+="input/by-id/$env{ID_BUS}-$env{ID_SERIAL}-if$attr{bInterfaceNumber}-event-$env{.INPUT_CLASS}"
# allow empty class for USB devices, by appending the interface number
SUBSYSTEMS=="usb", ENV{ID_BUS}=="?*", KERNEL=="event*", ENV{.INPUT_CLASS}=="", ATTRS{bInterfaceNumber}=="?*", \
SYMLINK+="input/by-id/$env{ID_BUS}-$env{ID_SERIAL}-event-if$attr{bInterfaceNumber}"
# by-path
SUBSYSTEMS=="pci|usb|platform|acpi", IMPORT{builtin}="path_id"
ENV{ID_PATH}=="?*", KERNEL=="mouse*|js*", ENV{.INPUT_CLASS}=="?*", SYMLINK+="input/by-path/$env{ID_PATH}-$env{.INPUT_CLASS}"
ENV{ID_PATH}=="?*", KERNEL=="event*", ENV{.INPUT_CLASS}=="?*", SYMLINK+="input/by-path/$env{ID_PATH}-event-$env{.INPUT_CLASS}"
# allow empty class for platform, usb and i2c devices; platform supports only a single interface that way
SUBSYSTEMS=="usb|platform|i2c", ENV{ID_PATH}=="?*", KERNEL=="event*", ENV{.INPUT_CLASS}=="", \
SYMLINK+="input/by-path/$env{ID_PATH}-event"
LABEL="persistent_input_end"
@@ -0,0 +1,40 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/60-persistent-storage-tape.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
# persistent storage links: /dev/tape/{by-id,by-path}
ACTION=="remove", GOTO="persistent_storage_tape_end"
ENV{UDEV_DISABLE_PERSISTENT_STORAGE_RULES_FLAG}=="1", GOTO="persistent_storage_tape_end"
# type 8 devices are "Medium Changers"
SUBSYSTEM=="scsi_generic", SUBSYSTEMS=="scsi", ATTRS{type}=="8", IMPORT{program}="scsi_id --sg-version=3 --export --whitelisted -d $devnode", \
SYMLINK+="tape/by-id/scsi-$env{ID_SERIAL} tape/by-id/scsi-$env{ID_SERIAL}-changer"
# iSCSI devices from the same host have all the same ID_SERIAL,
# but additionally a property named ID_SCSI_SERIAL.
SUBSYSTEM=="scsi_generic", SUBSYSTEMS=="scsi", ATTRS{type}=="8", ENV{ID_SCSI_SERIAL}=="?*", \
SYMLINK+="tape/by-id/scsi-$env{ID_SCSI_SERIAL}"
SUBSYSTEM=="scsi_generic", SUBSYSTEMS=="scsi", ATTRS{type}=="8", IMPORT{builtin}="path_id", \
SYMLINK+="tape/by-path/$env{ID_PATH}-changer"
SUBSYSTEM!="scsi_tape", GOTO="persistent_storage_tape_end"
KERNEL=="st*[0-9]|nst*[0-9]", ATTRS{ieee1394_id}=="?*", ENV{ID_SERIAL}="$attr{ieee1394_id}", ENV{ID_BUS}="ieee1394"
KERNEL=="st*[0-9]|nst*[0-9]", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id"
KERNEL=="st*[0-9]|nst*[0-9]", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="scsi", KERNELS=="[0-9]*:*[0-9]", ENV{.BSG_DEV}="$root/bsg/$id"
KERNEL=="st*[0-9]|nst*[0-9]", ENV{ID_SERIAL}!="?*", IMPORT{program}="scsi_id --whitelisted --export --device=$env{.BSG_DEV}", ENV{ID_BUS}="scsi"
KERNEL=="st*[0-9]", ENV{ID_SERIAL}=="?*", SYMLINK+="tape/by-id/$env{ID_BUS}-$env{ID_SERIAL}", OPTIONS+="link_priority=10"
KERNEL=="st*[0-9]", ENV{ID_SCSI_SERIAL}=="?*", SYMLINK+="tape/by-id/$env{ID_BUS}-$env{ID_SCSI_SERIAL}"
KERNEL=="nst*[0-9]", ENV{ID_SERIAL}=="?*", SYMLINK+="tape/by-id/$env{ID_BUS}-$env{ID_SERIAL}-nst"
KERNEL=="nst*[0-9]", ENV{ID_SCSI_SERIAL}=="?*", SYMLINK+="tape/by-id/$env{ID_BUS}-$env{ID_SCSI_SERIAL}-nst"
# by-path (parent device path)
KERNEL=="st*[0-9]|nst*[0-9]", IMPORT{builtin}="path_id"
KERNEL=="st*[0-9]", ENV{ID_PATH}=="?*", SYMLINK+="tape/by-path/$env{ID_PATH}"
KERNEL=="nst*[0-9]", ENV{ID_PATH}=="?*", SYMLINK+="tape/by-path/$env{ID_PATH}-nst"
LABEL="persistent_storage_tape_end"
+133
View File
@@ -0,0 +1,133 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/60-persistent-storage.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
# persistent storage links: /dev/disk/{by-id,by-uuid,by-label,by-path}
# scheme based on "Linux persistent device names", 2004, Hannes Reinecke <hare@suse.de>
ACTION=="remove", GOTO="persistent_storage_end"
ENV{UDEV_DISABLE_PERSISTENT_STORAGE_RULES_FLAG}=="1", GOTO="persistent_storage_end"
SUBSYSTEM!="block|ubi", GOTO="persistent_storage_end"
KERNEL!="loop*|mmcblk*[0-9]|msblk*[0-9]|mspblk*[0-9]|nvme*|sd*|sr*|vd*|xvd*|bcache*|cciss*|dasd*|ubd*|ubi*|scm*|pmem*|nbd*|zd*", GOTO="persistent_storage_end"
# ignore partitions that span the entire disk
TEST=="whole_disk", GOTO="persistent_storage_end"
# For partitions import parent disk ID_* information, except ID_FS_*.
#
# This is particularly important on media where a filesystem superblock and
# partition table are found on the same level, e.g. common Linux distro ISO
# installation media.
#
# In the case where a partition device points to the same filesystem that
# was detected on the parent disk, the ID_FS_* information is already
# present on the partition devices as well as the parent, so no need to
# propagate it. In the case where the partition device points to a different
# filesystem, merging the parent ID_FS_ properties would lead to
# inconsistencies, so we avoid doing so.
ENV{DEVTYPE}=="partition", \
IMPORT{parent}="ID_[!F]*", IMPORT{parent}="ID_", \
IMPORT{parent}="ID_F[!S]*", IMPORT{parent}="ID_F", \
IMPORT{parent}="ID_FS[!_]*", IMPORT{parent}="ID_FS"
# NVMe
KERNEL=="nvme*[0-9]n*[0-9]", ATTR{wwid}=="?*", SYMLINK+="disk/by-id/nvme-$attr{wwid}"
KERNEL=="nvme*[0-9]n*[0-9]p*[0-9]", ENV{DEVTYPE}=="partition", ATTRS{wwid}=="?*", SYMLINK+="disk/by-id/nvme-$attr{wwid}-part%n"
KERNEL=="nvme*[0-9]n*[0-9]", ENV{DEVTYPE}=="disk", ATTRS{serial}=="?*", ENV{ID_SERIAL_SHORT}="$attr{serial}"
KERNEL=="nvme*[0-9]n*[0-9]", ENV{DEVTYPE}=="disk", ATTRS{wwid}=="?*", ENV{ID_WWN}="$attr{wwid}"
KERNEL=="nvme*[0-9]n*[0-9]", ENV{DEVTYPE}=="disk", ATTRS{model}=="?*", ENV{ID_MODEL}="$attr{model}"
KERNEL=="nvme*[0-9]n*[0-9]", ENV{DEVTYPE}=="disk", ATTRS{firmware_rev}=="?*", ENV{ID_REVISION}="$attr{firmware_rev}"
KERNEL=="nvme*[0-9]n*[0-9]", ENV{DEVTYPE}=="disk", ENV{ID_MODEL}=="?*", ENV{ID_SERIAL_SHORT}=="?*", \
OPTIONS="string_escape=replace", ENV{ID_SERIAL}="$env{ID_MODEL}_$env{ID_SERIAL_SHORT}", SYMLINK+="disk/by-id/nvme-$env{ID_SERIAL}"
KERNEL=="nvme*[0-9]n*[0-9]p*[0-9]", ENV{DEVTYPE}=="partition", ATTRS{serial}=="?*", ENV{ID_SERIAL_SHORT}="$attr{serial}"
KERNEL=="nvme*[0-9]n*[0-9]p*[0-9]", ENV{DEVTYPE}=="partition", ATTRS{model}=="?*", ENV{ID_MODEL}="$attr{model}"
KERNEL=="nvme*[0-9]n*[0-9]p*[0-9]", ENV{DEVTYPE}=="partition", ATTRS{firmware_rev}=="?*", ENV{ID_REVISION}="$attr{firmware_rev}"
KERNEL=="nvme*[0-9]n*[0-9]p*[0-9]", ENV{DEVTYPE}=="partition", ENV{ID_MODEL}=="?*", ENV{ID_SERIAL_SHORT}=="?*", \
OPTIONS="string_escape=replace", ENV{ID_SERIAL}="$env{ID_MODEL}_$env{ID_SERIAL_SHORT}", SYMLINK+="disk/by-id/nvme-$env{ID_SERIAL}-part%n"
# virtio-blk
KERNEL=="vd*[!0-9]", ATTRS{serial}=="?*", ENV{ID_SERIAL}="$attr{serial}", SYMLINK+="disk/by-id/virtio-$env{ID_SERIAL}"
KERNEL=="vd*[0-9]", ATTRS{serial}=="?*", ENV{ID_SERIAL}="$attr{serial}", SYMLINK+="disk/by-id/virtio-$env{ID_SERIAL}-part%n"
# ATA
KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="scsi", ATTRS{vendor}=="ATA", IMPORT{program}="ata_id --export $devnode"
# ATAPI devices (SPC-3 or later)
KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="scsi", ATTRS{type}=="5", ATTRS{scsi_level}=="[6-9]*", IMPORT{program}="ata_id --export $devnode"
# Run ata_id on non-removable USB Mass Storage (SATA/PATA disks in enclosures)
KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", ATTR{removable}=="0", SUBSYSTEMS=="usb", IMPORT{program}="ata_id --export $devnode"
# Fall back usb_id for USB devices
KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id"
# SCSI devices
KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", IMPORT{program}="scsi_id --export --whitelisted -d $devnode", ENV{ID_BUS}="scsi"
KERNEL=="cciss*", ENV{DEVTYPE}=="disk", ENV{ID_SERIAL}!="?*", IMPORT{program}="scsi_id --export --whitelisted -d $devnode", ENV{ID_BUS}="cciss"
KERNEL=="sd*|sr*|cciss*", ENV{DEVTYPE}=="disk", ENV{ID_SERIAL}=="?*", SYMLINK+="disk/by-id/$env{ID_BUS}-$env{ID_SERIAL}"
KERNEL=="sd*|cciss*", ENV{DEVTYPE}=="partition", ENV{ID_SERIAL}=="?*", SYMLINK+="disk/by-id/$env{ID_BUS}-$env{ID_SERIAL}-part%n"
# PMEM devices
KERNEL=="pmem*", ENV{DEVTYPE}=="disk", ATTRS{uuid}=="?*", SYMLINK+="disk/by-id/pmem-$attr{uuid}"
# FireWire
KERNEL=="sd*[!0-9]|sr*", ATTRS{ieee1394_id}=="?*", SYMLINK+="disk/by-id/ieee1394-$attr{ieee1394_id}"
KERNEL=="sd*[0-9]", ATTRS{ieee1394_id}=="?*", SYMLINK+="disk/by-id/ieee1394-$attr{ieee1394_id}-part%n"
# MMC
KERNEL=="mmcblk[0-9]", SUBSYSTEMS=="mmc", ATTRS{serial}=="?*", ENV{ID_SERIAL}="$attr{serial}"
KERNEL=="mmcblk[0-9]", SUBSYSTEMS=="mmc", ATTRS{name}=="?*", ENV{ID_NAME}="$attr{name}"
KERNEL=="mmcblk[0-9]", ENV{ID_NAME}=="?*", ENV{ID_SERIAL}=="?*", SYMLINK+="disk/by-id/mmc-$env{ID_NAME}_$env{ID_SERIAL}"
KERNEL=="mmcblk[0-9]p[0-9]*", ENV{ID_NAME}=="?*", ENV{ID_SERIAL}=="?*", SYMLINK+="disk/by-id/mmc-$env{ID_NAME}_$env{ID_SERIAL}-part%n"
# Memstick
KERNEL=="msblk[0-9]|mspblk[0-9]", SUBSYSTEMS=="memstick", ATTRS{name}=="?*", ATTRS{serial}=="?*", \
ENV{ID_NAME}="$attr{name}", ENV{ID_SERIAL}="$attr{serial}", SYMLINK+="disk/by-id/memstick-$env{ID_NAME}_$env{ID_SERIAL}"
KERNEL=="msblk[0-9]p[0-9]|mspblk[0-9]p[0-9]", ENV{ID_NAME}=="?*", ENV{ID_SERIAL}=="?*", SYMLINK+="disk/by-id/memstick-$env{ID_NAME}_$env{ID_SERIAL}-part%n"
# by-path
ENV{DEVTYPE}=="disk", DEVPATH!="*/virtual/*", IMPORT{builtin}="path_id"
KERNEL=="mmcblk[0-9]boot[0-9]", ENV{DEVTYPE}=="disk", ENV{ID_PATH}=="?*", SYMLINK+="disk/by-path/$env{ID_PATH}-boot%n"
KERNEL!="mmcblk[0-9]boot[0-9]", ENV{DEVTYPE}=="disk", ENV{ID_PATH}=="?*", SYMLINK+="disk/by-path/$env{ID_PATH}"
ENV{DEVTYPE}=="partition", ENV{ID_PATH}=="?*", SYMLINK+="disk/by-path/$env{ID_PATH}-part%n"
# compatible links for ATA devices
KERNEL!="mmcblk[0-9]boot[0-9]", ENV{DEVTYPE}=="disk", ENV{ID_PATH_ATA_COMPAT}=="?*", SYMLINK+="disk/by-path/$env{ID_PATH_ATA_COMPAT}"
ENV{DEVTYPE}=="partition", ENV{ID_PATH_ATA_COMPAT}=="?*", SYMLINK+="disk/by-path/$env{ID_PATH_ATA_COMPAT}-part%n"
# legacy virtio-pci by-path links (deprecated)
KERNEL=="vd*[!0-9]", ENV{ID_PATH}=="pci-*", SYMLINK+="disk/by-path/virtio-$env{ID_PATH}"
KERNEL=="vd*[0-9]", ENV{ID_PATH}=="pci-*", SYMLINK+="disk/by-path/virtio-$env{ID_PATH}-part%n"
# probe filesystem metadata of optical drives which have a media inserted
KERNEL=="sr*", ENV{DISK_EJECT_REQUEST}!="?*", ENV{ID_CDROM_MEDIA_TRACK_COUNT_DATA}=="?*", ENV{ID_CDROM_MEDIA_SESSION_LAST_OFFSET}=="?*", \
IMPORT{builtin}="blkid --hint=session_offset=$env{ID_CDROM_MEDIA_SESSION_LAST_OFFSET}"
# single-session CDs do not have ID_CDROM_MEDIA_SESSION_LAST_OFFSET
KERNEL=="sr*", ENV{DISK_EJECT_REQUEST}!="?*", ENV{ID_CDROM_MEDIA_TRACK_COUNT_DATA}=="?*", ENV{ID_CDROM_MEDIA_SESSION_LAST_OFFSET}=="", \
IMPORT{builtin}="blkid --noraid"
# probe filesystem metadata of disks
KERNEL!="sr*|mmcblk[0-9]boot[0-9]", IMPORT{builtin}="blkid"
# by-label/by-uuid links (filesystem metadata)
ENV{ID_FS_USAGE}=="filesystem|other|crypto", ENV{ID_FS_UUID_ENC}=="?*", SYMLINK+="disk/by-uuid/$env{ID_FS_UUID_ENC}"
ENV{ID_FS_USAGE}=="filesystem|other|crypto", ENV{ID_FS_LABEL_ENC}=="?*", SYMLINK+="disk/by-label/$env{ID_FS_LABEL_ENC}"
# by-id (World Wide Name)
ENV{DEVTYPE}=="disk", ENV{ID_WWN_WITH_EXTENSION}=="?*", SYMLINK+="disk/by-id/wwn-$env{ID_WWN_WITH_EXTENSION}"
ENV{DEVTYPE}=="partition", ENV{ID_WWN_WITH_EXTENSION}=="?*", SYMLINK+="disk/by-id/wwn-$env{ID_WWN_WITH_EXTENSION}-part%n"
# by-partlabel/by-partuuid links (partition metadata)
ENV{ID_PART_ENTRY_UUID}=="?*", SYMLINK+="disk/by-partuuid/$env{ID_PART_ENTRY_UUID}"
ENV{ID_PART_ENTRY_SCHEME}=="gpt", ENV{ID_PART_ENTRY_NAME}=="?*", SYMLINK+="disk/by-partlabel/$env{ID_PART_ENTRY_NAME}"
# by-diskseq link (if an app is told to open a path like this, they may parse
# the diskseq number from the path, then issue BLKGETDISKSEQ to verify they really got
# the right device, to access specific disks in a race-free fashion)
ENV{DISKSEQ}=="?*", ENV{DEVTYPE}!="partition", SYMLINK+="disk/by-diskseq/$env{DISKSEQ}"
LABEL="persistent_storage_end"
+24
View File
@@ -0,0 +1,24 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/60-persistent-v4l.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="persistent_v4l_end"
SUBSYSTEM!="video4linux", GOTO="persistent_v4l_end"
ENV{MAJOR}=="", GOTO="persistent_v4l_end"
IMPORT{program}="v4l_id $devnode"
SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id"
KERNEL=="video*", ENV{ID_SERIAL}=="?*", SYMLINK+="v4l/by-id/$env{ID_BUS}-$env{ID_SERIAL}-video-index$attr{index}"
# check for valid "index" number
TEST!="index", GOTO="persistent_v4l_end"
ATTR{index}!="?*", GOTO="persistent_v4l_end"
IMPORT{builtin}="path_id"
ENV{ID_PATH}=="?*", KERNEL=="video*|vbi*", SYMLINK+="v4l/by-path/$env{ID_PATH}-video-index$attr{index}"
ENV{ID_PATH}=="?*", KERNEL=="audio*", SYMLINK+="v4l/by-path/$env{ID_PATH}-audio-index$attr{index}"
LABEL="persistent_v4l_end"
+38
View File
@@ -0,0 +1,38 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/60-sensor.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="sensor_end"
# device matching the sensor's label, name and the machine's DMI data for IIO devices
SUBSYSTEM=="iio", KERNEL=="iio*", SUBSYSTEMS=="usb|i2c|platform", ATTR{label}!="", \
IMPORT{builtin}="hwdb 'sensor:$attr{label}:modalias:$attr{modalias}:$attr{[dmi/id]modalias}'", \
GOTO="sensor_end"
# Before Linux v6.0, cros-ec-accel used a non-standard 'location' sysfs file
SUBSYSTEM=="iio", KERNEL=="iio*", SUBSYSTEMS=="platform", \
ATTR{name}=="cros-ec-accel|cros-ec-accel-legacy", ATTR{location}=="base", \
IMPORT{builtin}="hwdb 'sensor:accel-base:modalias:$attr{modalias}:$attr{[dmi/id]modalias}'", \
GOTO="sensor_end"
SUBSYSTEM=="iio", KERNEL=="iio*", SUBSYSTEMS=="platform", \
ATTR{name}=="cros-ec-accel|cros-ec-accel-legacy", ATTR{location}=="lid", \
IMPORT{builtin}="hwdb 'sensor:accel-display:modalias:$attr{modalias}:$attr{[dmi/id]modalias}'", \
GOTO="sensor_end"
# device matching the sensor's name and the machine's DMI data for IIO devices
SUBSYSTEM=="iio", KERNEL=="iio*", SUBSYSTEMS=="usb|i2c|platform", \
IMPORT{builtin}="hwdb 'sensor:modalias:$attr{modalias}:$attr{[dmi/id]modalias}'", \
GOTO="sensor_end"
SUBSYSTEM=="input", ENV{ID_INPUT_ACCELEROMETER}=="1", SUBSYSTEMS=="acpi", \
IMPORT{builtin}="hwdb 'sensor:modalias:acpi:$attr{hid}:$attr{[dmi/id]modalias}'", \
GOTO="sensor_end"
SUBSYSTEM=="input", ENV{ID_INPUT_ACCELEROMETER}=="1", SUBSYSTEMS=="platform", \
IMPORT{builtin}="hwdb 'sensor:modalias:platform:$id:$attr{[dmi/id]modalias}'", \
GOTO="sensor_end"
LABEL="sensor_end"
+30
View File
@@ -0,0 +1,30 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/60-serial.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="serial_end"
SUBSYSTEM!="tty", GOTO="serial_end"
SUBSYSTEMS=="pci", ENV{ID_BUS}="pci", ENV{ID_VENDOR_ID}="$attr{vendor}", ENV{ID_MODEL_ID}="$attr{device}"
SUBSYSTEMS=="pci", IMPORT{builtin}="hwdb --subsystem=pci"
SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id", IMPORT{builtin}="hwdb --subsystem=usb"
# /dev/serial/by-path/, /dev/serial/by-id/ for USB devices
KERNEL!="ttyUSB[0-9]*|ttyACM[0-9]*", GOTO="serial_end"
SUBSYSTEMS=="usb-serial", ENV{.ID_PORT}="$attr{port_number}"
IMPORT{builtin}="path_id"
ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="", SYMLINK+="serial/by-path/$env{ID_PATH}"
ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="?*", SYMLINK+="serial/by-path/$env{ID_PATH}-port$env{.ID_PORT}"
IMPORT{builtin}="usb_id"
ENV{ID_SERIAL}=="", GOTO="serial_end"
SUBSYSTEMS=="usb", ENV{ID_USB_INTERFACE_NUM}="$attr{bInterfaceNumber}"
ENV{ID_USB_INTERFACE_NUM}=="", GOTO="serial_end"
ENV{.ID_PORT}=="", SYMLINK+="serial/by-id/$env{ID_BUS}-$env{ID_SERIAL}-if$env{ID_USB_INTERFACE_NUM}"
ENV{.ID_PORT}=="?*", SYMLINK+="serial/by-id/$env{ID_BUS}-$env{ID_SERIAL}-if$env{ID_USB_INTERFACE_NUM}-port$env{.ID_PORT}"
LABEL="serial_end"
+21
View File
@@ -0,0 +1,21 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/64-btrfs.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
SUBSYSTEM!="block", GOTO="btrfs_end"
ACTION=="remove", GOTO="btrfs_end"
ENV{ID_FS_TYPE}!="btrfs", GOTO="btrfs_end"
ENV{SYSTEMD_READY}=="0", GOTO="btrfs_end"
# let the kernel know about this btrfs filesystem, and check if it is complete
IMPORT{builtin}="btrfs ready $devnode"
# mark the device as not ready to be used by the system
ENV{ID_BTRFS_READY}=="0", ENV{SYSTEMD_READY}="0"
# reconsider pending devices in case when multidevice volume awaits
ENV{ID_BTRFS_READY}=="1", RUN+="/usr/bin/udevadm trigger -s block -p ID_BTRFS_READY=0"
LABEL="btrfs_end"
+13
View File
@@ -0,0 +1,13 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/70-camera.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="camera_end"
SUBSYSTEM=="video4linux", ENV{ID_BUS}="usb" , \
IMPORT{builtin}="hwdb 'camera:usb:v$env{ID_VENDOR_ID}p$env{ID_MODEL_ID}:name:$attr{name}:'", \
GOTO="camera_end"
LABEL="camera_end"
+16
View File
@@ -0,0 +1,16 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/70-joystick.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="joystick_end"
ENV{ID_INPUT_JOYSTICK}=="", GOTO="joystick_end"
KERNEL!="event*", GOTO="joystick_end"
# joystick:<bustype>:v<vid>p<pid>:name:<name>:*
KERNELS=="input*", ENV{ID_BUS}!="", \
IMPORT{builtin}="hwdb 'joystick:$env{ID_BUS}:v$attr{id/vendor}p$attr{id/product}:name:$attr{name}:'", \
GOTO="joystick_end"
LABEL="joystick_end"
+12
View File
@@ -0,0 +1,12 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/70-memory.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="memory_end"
SUBSYSTEM!="dmi", GOTO="memory_end"
IMPORT{program}="dmi_memory_id"
LABEL="memory_end"
+22
View File
@@ -0,0 +1,22 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/70-mouse.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="mouse_end"
KERNEL!="event*", GOTO="mouse_end"
ENV{ID_INPUT_MOUSE}=="", GOTO="mouse_end"
# mouse:<subsystem>:v<vid>p<pid>:name:<name>:*
KERNELS=="input*", ENV{ID_BUS}=="usb", \
IMPORT{builtin}="hwdb 'mouse:$env{ID_BUS}:v$attr{id/vendor}p$attr{id/product}:name:$attr{name}:'", \
GOTO="mouse_end"
KERNELS=="input*", ENV{ID_BUS}=="bluetooth", \
IMPORT{builtin}="hwdb 'mouse:$env{ID_BUS}:v$attr{id/vendor}p$attr{id/product}:name:$attr{name}:'", \
GOTO="mouse_end"
DRIVERS=="psmouse", SUBSYSTEMS=="serio", \
IMPORT{builtin}="hwdb 'mouse:ps2::name:$attr{device/name}:'", \
GOTO="mouse_end"
LABEL="mouse_end"
+17
View File
@@ -0,0 +1,17 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/70-touchpad.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="touchpad_end"
ENV{ID_INPUT}=="", GOTO="touchpad_end"
ENV{ID_INPUT_TOUCHPAD}=="", GOTO="touchpad_end"
KERNEL!="event*", GOTO="touchpad_end"
# touchpad:<subsystem>:v<vid>p<pid>:name:<name>:*
KERNELS=="input*", ENV{ID_BUS}!="", \
IMPORT{builtin}="hwdb 'touchpad:$env{ID_BUS}:v$attr{id/vendor}p$attr{id/product}:name:$attr{name}:'", \
GOTO="touchpad_end"
LABEL="touchpad_end"
+18
View File
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/75-net-description.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="net_end"
SUBSYSTEM!="net", GOTO="net_end"
IMPORT{builtin}="net_id"
SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id", IMPORT{builtin}="hwdb --subsystem=usb"
SUBSYSTEMS=="usb", GOTO="net_end"
SUBSYSTEMS=="pci", ENV{ID_BUS}="pci", ENV{ID_VENDOR_ID}="$attr{vendor}", ENV{ID_MODEL_ID}="$attr{device}"
SUBSYSTEMS=="pci", IMPORT{builtin}="hwdb --subsystem=pci"
LABEL="net_end"
+11
View File
@@ -0,0 +1,11 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/75-probe_mtd.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION!="add", GOTO="mtd_probe_end"
KERNEL=="mtd*ro", IMPORT{program}="mtd_probe $devnode"
LABEL="mtd_probe_end"
+100
View File
@@ -0,0 +1,100 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/78-sound-card.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
SUBSYSTEM!="sound", GOTO="sound_end"
ACTION=="add|change", KERNEL=="controlC*", ATTR{../uevent}="change"
ACTION!="change", GOTO="sound_end"
# Ok, we probably need a little explanation here for what the two lines above
# are good for.
#
# The story goes like this: when ALSA registers a new sound card it emits a
# series of 'add' events to userspace, for the main card device and for all the
# child device nodes that belong to it. udev relays those to applications,
# however only maintains the order between father and child, but not between
# the siblings. The control device node creation can be used as synchronization
# point. All other devices that belong to a card are created in the kernel
# before it. However unfortunately due to the fact that siblings are forwarded
# out of order by udev this fact is lost to applications.
#
# OTOH before an application can open a device it needs to make sure that all
# its device nodes are completely created and set up.
#
# As a workaround for this issue we have added the udev rule above which will
# generate a 'change' event on the main card device from the 'add' event of the
# card's control device. Due to the ordering semantics of udev this event will
# only be relayed after all child devices have finished processing properly.
# When an application needs to listen for appearing devices it can hence look
# for 'change' events only, and ignore the actual 'add' events.
#
# When the application is initialized at the same time as a device is plugged
# in it may need to figure out if the 'change' event has already been triggered
# or not for a card. To find that out we store the flag environment variable
# SOUND_INITIALIZED on the device which simply tells us if the card 'change'
# event has already been processed.
KERNEL!="card*", GOTO="sound_end"
ENV{SOUND_INITIALIZED}="1"
IMPORT{builtin}="hwdb"
SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id"
SUBSYSTEMS=="usb", GOTO="skip_pci"
SUBSYSTEMS=="firewire", ATTRS{guid}=="?*", \
ENV{ID_BUS}="firewire", ENV{ID_SERIAL}="$attr{guid}", ENV{ID_SERIAL_SHORT}="$attr{guid}", \
ENV{ID_VENDOR_ID}="$attr{vendor}", ENV{ID_MODEL_ID}="$attr{model}", \
ENV{ID_VENDOR}="$attr{vendor_name}", ENV{ID_MODEL}="$attr{model_name}"
SUBSYSTEMS=="firewire", GOTO="skip_pci"
SUBSYSTEMS=="pci", ENV{ID_BUS}="pci", ENV{ID_VENDOR_ID}="$attr{vendor}", ENV{ID_MODEL_ID}="$attr{device}"
SUBSYSTEMS=="pci", GOTO="skip_pci"
# If we reach here, the device nor any of its parents are USB/PCI/firewire bus devices.
# If we now find a parent that is a platform device, assume that we're working with
# an internal sound card.
SUBSYSTEMS=="platform", ENV{SOUND_FORM_FACTOR}="internal", GOTO="sound_end"
LABEL="skip_pci"
# Define ID_ID if ID_BUS and ID_SERIAL are set. This will work for both
# USB and firewire.
ENV{ID_SERIAL}=="?*", ENV{ID_USB_INTERFACE_NUM}=="?*", ENV{ID_ID}="$env{ID_BUS}-$env{ID_SERIAL}-$env{ID_USB_INTERFACE_NUM}"
ENV{ID_SERIAL}=="?*", ENV{ID_USB_INTERFACE_NUM}=="", ENV{ID_ID}="$env{ID_BUS}-$env{ID_SERIAL}"
IMPORT{builtin}="path_id"
# The values used here for $SOUND_FORM_FACTOR and $SOUND_CLASS should be kept
# in sync with those defined for PulseAudio's src/pulse/proplist.h
# PA_PROP_DEVICE_FORM_FACTOR, PA_PROP_DEVICE_CLASS properties.
# If the first PCM device of this card has the pcm class 'modem', then the card is a modem
ATTR{pcmC%nD0p/pcm_class}=="modem", ENV{SOUND_CLASS}="modem", GOTO="sound_end"
# Identify cards on the internal PCI bus as internal
SUBSYSTEMS=="pci", DEVPATH=="*/0000:00:??.?/sound/*", ENV{SOUND_FORM_FACTOR}="internal", GOTO="sound_end"
# Devices that also support Image/Video interfaces are most likely webcams
SUBSYSTEMS=="usb", ENV{ID_USB_INTERFACES}=="*:0e????:*", ENV{SOUND_FORM_FACTOR}="webcam", GOTO="sound_end"
# Matching on the model strings is a bit ugly, I admit
ENV{ID_MODEL}=="*[Ss]peaker*", ENV{SOUND_FORM_FACTOR}="speaker", GOTO="sound_end"
ENV{ID_MODEL_FROM_DATABASE}=="*[Ss]peaker*", ENV{SOUND_FORM_FACTOR}="speaker", GOTO="sound_end"
ENV{ID_MODEL}=="*[Hh]eadphone*", ENV{SOUND_FORM_FACTOR}="headphone", GOTO="sound_end"
ENV{ID_MODEL_FROM_DATABASE}=="*[Hh]eadphone*", ENV{SOUND_FORM_FACTOR}="headphone", GOTO="sound_end"
ENV{ID_MODEL}=="*[Hh]eadset*", ENV{SOUND_FORM_FACTOR}="headset", GOTO="sound_end"
ENV{ID_MODEL_FROM_DATABASE}=="*[Hh]eadset*", ENV{SOUND_FORM_FACTOR}="headset", GOTO="sound_end"
ENV{ID_MODEL}=="*[Hh]andset*", ENV{SOUND_FORM_FACTOR}="handset", GOTO="sound_end"
ENV{ID_MODEL_FROM_DATABASE}=="*[Hh]andset*", ENV{SOUND_FORM_FACTOR}="handset", GOTO="sound_end"
ENV{ID_MODEL}=="*[Mm]icrophone*", ENV{SOUND_FORM_FACTOR}="microphone", GOTO="sound_end"
ENV{ID_MODEL_FROM_DATABASE}=="*[Mm]icrophone*", ENV{SOUND_FORM_FACTOR}="microphone", GOTO="sound_end"
LABEL="sound_end"
+17
View File
@@ -0,0 +1,17 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/80-drivers.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION!="add", GOTO="drivers_end"
ENV{MODALIAS}=="?*", RUN{builtin}+="kmod load '$env{MODALIAS}'"
SUBSYSTEM=="tifm", ENV{TIFM_CARD_TYPE}=="SD", RUN{builtin}+="kmod load tifm_sd"
SUBSYSTEM=="tifm", ENV{TIFM_CARD_TYPE}=="MS", RUN{builtin}+="kmod load tifm_ms"
SUBSYSTEM=="memstick", RUN{builtin}+="kmod load ms_block mspro_block"
SUBSYSTEM=="i2o", RUN{builtin}+="kmod load i2o_block"
SUBSYSTEM=="module", KERNEL=="parport_pc", RUN{builtin}+="kmod load ppdev"
KERNEL=="mtd*ro", ENV{MTD_FTL}=="smartmedia", RUN{builtin}+="kmod load sm_ftl"
LABEL="drivers_end"
+18
View File
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/80-net-name-slot.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION!="add", GOTO="net_name_slot_end"
SUBSYSTEM!="net", GOTO="net_name_slot_end"
NAME!="", GOTO="net_name_slot_end"
IMPORT{cmdline}="net.ifnames"
ENV{net.ifnames}=="0", GOTO="net_name_slot_end"
NAME=="", ENV{ID_NET_NAME_ONBOARD}!="", NAME="$env{ID_NET_NAME_ONBOARD}"
NAME=="", ENV{ID_NET_NAME_SLOT}!="", NAME="$env{ID_NET_NAME_SLOT}"
NAME=="", ENV{ID_NET_NAME_PATH}!="", NAME="$env{ID_NET_NAME_PATH}"
LABEL="net_name_slot_end"
+18
View File
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Derived from eudev 3.2.14 rules/81-net-dhcp.rules
# Modifications by the Finit project: none
#
# do not edit this file, it will be overwritten on update
ACTION=="remove", GOTO="net_dhcp_end"
SUBSYSTEM!="net", GOTO="net_dhcp_end"
# Network interfaces requiring DHCPOFFER messages to be broadcast
# must set ID_NET_DHCP_BROADCAST to "1". This property will be
# checked by the networkd DHCP4 client to set the DHCP option
# s390 ccwgroup interfaces in layer3 mode need broadcast DHCPOFFER
# using the link driver to detect this condition
ENV{ID_NET_DRIVER}=="qeth_l3", ENV{ID_NET_DHCP_BROADCAST}="1"
LABEL="net_dhcp_end"
+163
View File
@@ -0,0 +1,163 @@
/* udev rules file parser for keventd
*
* Copyright (c) 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.
*/
#ifndef KEVENTD_RULES_H_
#define KEVENTD_RULES_H_
#ifdef _LIBITE_LITE
# include <libite/queue.h>
#else
# include <lite/queue.h>
#endif
#include "keventd.h"
/* Operators from udev rules syntax */
typedef enum {
OP_INVALID = 0,
OP_MATCH_EQ, /* == */
OP_MATCH_NE, /* != */
OP_ASSIGN, /* = */
OP_ASSIGN_ADD, /* += */
OP_ASSIGN_DEL, /* -= */
OP_ASSIGN_FINAL, /* := */
} rule_op_t;
/* All udev match and assignment key types */
typedef enum {
KEY_UNKNOWN = 0,
/* Match keys — current device */
KEY_ACTION,
KEY_DEVPATH,
KEY_KERNEL,
KEY_NAME,
KEY_SUBSYSTEM,
KEY_DRIVER,
KEY_ATTR, /* ATTR{filename} */
KEY_SYSCTL, /* SYSCTL{parameter} */
KEY_ENV, /* ENV{variable} */
KEY_CONST, /* CONST{key} */
KEY_TAG,
KEY_TEST, /* TEST{mode} */
KEY_PROGRAM,
KEY_RESULT,
/* Match keys — parent-chain walk */
KEY_KERNELS,
KEY_SUBSYSTEMS,
KEY_DRIVERS,
KEY_ATTRS, /* ATTRS{filename} */
KEY_TAGS,
/* Assignment keys */
KEY_SYMLINK,
KEY_OWNER,
KEY_GROUP,
KEY_MODE,
KEY_SECLABEL, /* SECLABEL{module} */
KEY_RUN, /* RUN{type} */
KEY_LABEL,
KEY_GOTO,
KEY_IMPORT, /* IMPORT{type} */
KEY_OPTIONS,
} rule_key_class_t;
/* Pattern matching strategy for match-op keys */
typedef enum {
PAT_PLAIN = 0, /* literal strcmp */
PAT_GLOB, /* fnmatch */
PAT_SPLIT, /* pipe-separated alternatives */
} pat_type_t;
/* IMPORT{type} subtypes */
typedef enum {
IMPORT_PROGRAM = 0, /* run program, parse KEY=VALUE output */
IMPORT_FILE, /* read KEY=VALUE from file */
IMPORT_DB, /* restore properties from udev database */
IMPORT_BUILTIN, /* run builtin, parse output */
IMPORT_PARENT, /* copy from parent device's database entry */
IMPORT_CMDLINE, /* import matching key from /proc/cmdline */
} import_type_t;
/* One key–operator–value triplet within a rule */
struct rule_key {
rule_key_class_t cls;
rule_op_t op;
char *attr; /* ATTR{x}/ENV{x}/… — heap-allocated, may be NULL */
char *value; /* pattern or assignment value — heap-allocated */
pat_type_t pat_type;
import_type_t import_type;
run_type_t run_type;
};
#define RULE_KEY_MAX 32
/* One parsed rule (a single non-empty, non-comment line) */
struct rule {
TAILQ_ENTRY(rule) link;
char *filename; /* heap-allocated source path */
int lineno;
int nkeys;
struct rule_key keys[RULE_KEY_MAX];
};
TAILQ_HEAD(rule_list, rule);
/*
* Load rules from a single .rules file, appending to list.
* Returns the number of rules loaded, or -1 on open failure.
*/
int rules_load (struct rule_list *list, const char *path);
/*
* Load all *.rules files from the standard udev directories
* (/lib/udev/rules.d, /run/udev/rules.d, /etc/udev/rules.d)
* plus extra_dir if non-NULL. Files are loaded in alphanumeric
* order within each directory; directories are processed in order.
* Returns total number of rules loaded.
*/
int rules_load_all(struct rule_list *list, const char *extra_dir);
/*
* Free all rules and release their resources.
*/
void rules_free (struct rule_list *list);
/*
* Apply all matching rules from list to ev, updating ev->applied,
* ev->result, env store, and tag list.
*/
int rules_apply (struct rule_list *list, struct uevent *ev);
/*
* Execute the RUN+= commands accumulated in ev->applied.run_cmds.
* Call after all device handling (nodes, symlinks) is complete.
*/
void rules_run_cmds(struct uevent *ev);
#endif /* KEVENTD_RULES_H_ */
/**
* Local Variables:
* indent-tabs-mode: t
* c-file-style: "linux"
* End:
*/
+131
View File
@@ -0,0 +1,131 @@
/* sysfs attribute reader and parent-chain walker
*
* Copyright (c) 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 <limits.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
# include <lite/lite.h>
#endif
#include "sysfs.h"
int sysfs_read_file(const char *path, char *buf, size_t len)
{
FILE *fp;
fp = fopen(path, "r");
if (!fp)
return -1;
if (!fgets(buf, len, fp)) {
fclose(fp);
return -1;
}
fclose(fp);
chomp(buf);
return 0;
}
int sysfs_read_attr(const char *devpath, const char *attr,
char *buf, size_t len)
{
char path[PATH_MAX];
snprintf(path, sizeof(path), "/sys%s/%s", devpath, attr);
return sysfs_read_file(path, buf, len);
}
int sysfs_parent_walk(const char *devpath,
int (*cb)(const char *syspath, void *data),
void *data)
{
char syspath[PATH_MAX];
char *p;
snprintf(syspath, sizeof(syspath), "/sys%s", devpath);
while (1) {
if (cb(syspath, data) == 0)
return 0;
/* Strip last path component */
p = strrchr(syspath, '/');
if (!p || p == syspath)
break;
*p = 0;
/* Do not walk above /sys/devices */
if (!strcmp(syspath, "/sys/devices") ||
!strcmp(syspath, "/sys"))
break;
}
return -1;
}
int sysfs_read_subsystem(const char *syspath, char *buf, size_t len)
{
char lpath[PATH_MAX], target[PATH_MAX];
const char *p;
ssize_t n;
snprintf(lpath, sizeof(lpath), "%s/subsystem", syspath);
n = readlink(lpath, target, sizeof(target) - 1);
if (n < 0)
return -1;
target[n] = 0;
p = strrchr(target, '/');
snprintf(buf, len, "%s", p ? p + 1 : target);
return 0;
}
int sysfs_read_driver(const char *syspath, char *buf, size_t len)
{
char lpath[PATH_MAX], target[PATH_MAX];
const char *p;
ssize_t n;
snprintf(lpath, sizeof(lpath), "%s/driver", syspath);
n = readlink(lpath, target, sizeof(target) - 1);
if (n < 0)
return -1;
target[n] = 0;
p = strrchr(target, '/');
snprintf(buf, len, "%s", p ? p + 1 : target);
return 0;
}
/**
* Local Variables:
* indent-tabs-mode: t
* c-file-style: "linux"
* End:
*/
+75
View File
@@ -0,0 +1,75 @@
/* sysfs attribute reader and parent-chain walker for keventd
*
* Copyright (c) 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.
*/
#ifndef KEVENTD_SYSFS_H_
#define KEVENTD_SYSFS_H_
#include <stddef.h>
/*
* Read the first line from an absolute path.
* Returns 0 on success, -1 on failure.
*/
int sysfs_read_file(const char *path, char *buf, size_t len);
/*
* Read a sysfs attribute for the device at /sys{devpath}.
* attr may be a relative path like "device/vendor" or "../serial".
* Returns 0 on success, -1 if the attribute does not exist.
*/
int sysfs_read_attr(const char *devpath, const char *attr,
char *buf, size_t len);
/*
* Walk the sysfs device tree upward from devpath toward /sys/devices,
* calling cb at each level. cb receives the absolute syspath and the
* caller-supplied data pointer. Walk stops when cb returns 0 (found)
* or the tree root is reached.
* Returns 0 if cb returned 0, -1 if nothing matched.
*/
int sysfs_parent_walk(const char *devpath,
int (*cb)(const char *syspath, void *data),
void *data);
/*
* Read the subsystem name at syspath by following the "subsystem"
* symlink and returning its basename.
* Returns 0 on success, -1 if there is no subsystem link.
*/
int sysfs_read_subsystem(const char *syspath, char *buf, size_t len);
/*
* Read the driver name at syspath by following the "driver" symlink
* and returning its basename.
* Returns 0 on success, -1 if there is no driver link.
*/
int sysfs_read_driver(const char *syspath, char *buf, size_t len);
#endif /* KEVENTD_SYSFS_H_ */
/**
* Local Variables:
* indent-tabs-mode: t
* c-file-style: "linux"
* End:
*/
+251
View File
@@ -0,0 +1,251 @@
/* udev-compatible per-device database in /run/udev/data/
*
* Copyright (c) 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 <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <syslog.h>
#include <unistd.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
# include <lite/lite.h>
#endif
#include "keventd.h"
#include "sysfs.h"
#include "udevdb.h"
/*
* Compute the database file path for a device.
*
* udev's keying scheme:
* b<maj>:<min> block device
* c<maj>:<min> character device
* n<ifindex> network interface
* +<subsystem>:<sysname> everything else
*
* This matches the layout that udevadm info, libudev, and eudev expect.
*/
static void udevdb_path(const struct uevent *ev, char *buf, size_t len)
{
const char *sysname = uevent_sysname(ev);
const char *ifindex = NULL;
if (ev->subsystem && !strcmp(ev->subsystem, "net"))
ifindex = uevent_getenv(ev, "IFINDEX");
if (ev->major >= 0 && ev->minor >= 0) {
const char *t = (ev->subsystem && !strcmp(ev->subsystem, "block"))
? "b" : "c";
snprintf(buf, len, UDEVDB_PATH "/%s%d:%d", t, ev->major, ev->minor);
} else if (ifindex) {
snprintf(buf, len, UDEVDB_PATH "/n%s", ifindex);
} else {
snprintf(buf, len, UDEVDB_PATH "/+%s:%s",
ev->subsystem ? ev->subsystem : "unknown",
sysname ? sysname : "unknown");
}
}
/*
* Write device properties to the database.
*
* Called after devnode_add() and symlink_add() so that all S: records
* can be captured via symlink_write_db().
*/
int udevdb_write(const struct uevent *ev)
{
char path[PATH_MAX];
struct timeval tv;
FILE *fp;
int i;
if (!ev->devpath)
return -1;
if (mkpath(UDEVDB_PATH, 0755) && errno != EEXIST)
return -1;
udevdb_path(ev, path, sizeof(path));
fp = fopen(path, "w");
if (!fp) {
logit(LOG_DEBUG, "udevdb: cannot write %s: %s", path, strerror(errno));
return -1;
}
/* Timestamp in microseconds since epoch */
gettimeofday(&tv, NULL);
fprintf(fp, "I:%llu\n",
(unsigned long long)(tv.tv_sec * 1000000ULL + tv.tv_usec));
/* All environment variables / device properties */
for (i = 0; i < ev->nenv; i++)
fprintf(fp, "E:%s=%s\n", ev->env_key[i], ev->env_val[i]);
/* Symlinks created for this device */
symlink_write_db(ev->devpath, fp);
/* Tags */
for (i = 0; i < ev->ntags; i++) {
fprintf(fp, "G:%s\n", ev->tags[i]);
fprintf(fp, "Q:%s\n", ev->tags[i]);
}
fprintf(fp, "V:1\n");
fclose(fp);
return 0;
}
/*
* Read E: records from a database file into ev's env store.
*/
static int udevdb_load(const char *path, struct uevent *ev)
{
char line[1024];
FILE *fp;
fp = fopen(path, "r");
if (!fp)
return -1;
while (fgets(line, sizeof(line), fp)) {
char *val;
chomp(line);
if (line[0] != 'E' || line[1] != ':')
continue;
val = strchr(line + 2, '=');
if (!val)
continue;
*val++ = 0;
uevent_setenv(ev, line + 2, val);
}
fclose(fp);
return 0;
}
/*
* Read device properties back from the database into ev's env store.
*
* Used by IMPORT{db} in the rules engine (Phase 4) to recover properties
* that were set during a previous event for the same device.
*/
int udevdb_read(struct uevent *ev)
{
char path[PATH_MAX];
if (!ev->devpath)
return -1;
udevdb_path(ev, path, sizeof(path));
return udevdb_load(path, ev);
}
/*
* Read E: records for any devpath, keyed from its sysfs attributes:
* the device has no uevent in hand, so major:minor, ifindex, and
* subsystem come from /sys. Serves IMPORT{parent} and the D-Bus
* Info method.
*/
int udevdb_read_devpath(const char *devpath, struct uevent *ev)
{
char syspath[PATH_MAX], dbfile[PATH_MAX];
char subsys[64] = "unknown";
char attr[64];
const char *sn;
if (!devpath || *devpath != '/')
return -1;
sn = strrchr(devpath, '/');
sn = sn ? sn + 1 : devpath;
snprintf(syspath, sizeof(syspath), "/sys%s", devpath);
sysfs_read_subsystem(syspath, subsys, sizeof(subsys));
snprintf(dbfile, sizeof(dbfile), "%s/dev", syspath);
if (!sysfs_read_file(dbfile, attr, sizeof(attr))) {
snprintf(dbfile, sizeof(dbfile), UDEVDB_PATH "/%s%s",
strcmp(subsys, "block") ? "c" : "b", attr);
} else if (!strcmp(subsys, "net")) {
snprintf(dbfile, sizeof(dbfile), "%s/ifindex", syspath);
if (sysfs_read_file(dbfile, attr, sizeof(attr)))
return -1;
snprintf(dbfile, sizeof(dbfile), UDEVDB_PATH "/n%s", attr);
} else {
snprintf(dbfile, sizeof(dbfile), UDEVDB_PATH "/+%s:%s",
subsys, sn);
}
return udevdb_load(dbfile, ev);
}
int udevdb_read_parent(struct uevent *ev)
{
char ppath[PATH_MAX];
char *slash;
if (!ev->devpath)
return -1;
snprintf(ppath, sizeof(ppath), "%s", ev->devpath);
slash = strrchr(ppath, '/');
if (!slash || slash == ppath)
return -1;
*slash = 0;
return udevdb_read_devpath(ppath, ev);
}
/*
* Remove a device's database entry on ACT_REMOVE.
*/
void udevdb_delete(const struct uevent *ev)
{
char path[PATH_MAX];
if (!ev->devpath)
return;
udevdb_path(ev, path, sizeof(path));
if (unlink(path) && errno != ENOENT)
logit(LOG_DEBUG, "udevdb: cannot remove %s: %s", path, strerror(errno));
}
/**
* Local Variables:
* indent-tabs-mode: t
* c-file-style: "linux"
* End:
*/
+77
View File
@@ -0,0 +1,77 @@
/* udev-compatible per-device database in /run/udev/data/
*
* Copyright (c) 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.
*/
#ifndef KEVENTD_UDEVDB_H_
#define KEVENTD_UDEVDB_H_
#include "keventd.h"
/*
* Database root directory, compatible with systemd/eudev's /run/udev/data/.
* One text file per device, keyed by device ID (see udevdb_path()).
*
* Record format (one per line):
* I:<usec> initialisation timestamp
* E:<KEY>=<VAL> environment / property
* S:<path> symlink (relative to /dev/)
* G:<tag> tag (for G: records)
* Q:<tag> tag (for Q: query records, mirrors G:)
* V:1 format version sentinel, always last
*/
#define UDEVDB_PATH "/run/udev/data"
/*
* Write a device's properties to the database after event processing.
* Creates /run/udev/data/<id> with I:, E:, S:, G:, Q:, V: records.
*/
int udevdb_write (const struct uevent *ev);
/*
* Read a device's properties from the database into ev's env store.
* Used by IMPORT{db} and IMPORT{parent} in the rules engine (Phase 4).
*/
int udevdb_read (struct uevent *ev);
/*
* Like udevdb_read(), but for the parent device of ev.
*/
int udevdb_read_parent (struct uevent *ev);
/*
* Like udevdb_read(), for any devpath, keyed from sysfs.
*/
int udevdb_read_devpath(const char *devpath, struct uevent *ev);
/*
* Delete a device's database entry on ACT_REMOVE.
*/
void udevdb_delete (const struct uevent *ev);
#endif /* KEVENTD_UDEVDB_H_ */
/**
* Local Variables:
* indent-tabs-mode: t
* c-file-style: "linux"
* End:
*/
+1221
View File
File diff suppressed because it is too large. Load diff
+35
View File
@@ -152,6 +152,13 @@ static void emit_property(struct xbuf *x, const link_property_t *p)
p->name, p->sig ? p->sig : "s");
}
static void emit_signal(struct xbuf *x, const link_signal_t *s)
{
xprintf(x, " <signal name=\"%s\">\n", s->name);
emit_args(x, s->sig, "out");
xprintf(x, " </signal>\n");
}
static const char STANDARD_INTERFACES_XML[] =
" <interface name=\"org.freedesktop.DBus.Introspectable\">\n"
" <method name=\"Introspect\">\n"
@@ -174,6 +181,28 @@ static const char STANDARD_INTERFACES_XML[] =
" <arg name=\"interface\" type=\"s\" direction=\"in\"/>\n"
" <arg name=\"props\" type=\"a{sv}\" direction=\"out\"/>\n"
" </method>\n"
" <signal name=\"PropertiesChanged\">\n"
" <arg name=\"interface\" type=\"s\"/>\n"
" <arg name=\"changed\" type=\"a{sv}\"/>\n"
" <arg name=\"invalidated\" type=\"as\"/>\n"
" </signal>\n"
" </interface>\n";
/*
* Hello/AddMatch/RemoveMatch are answered on the canonical
* /org/freedesktop/DBus object only, so declare them only there.
*/
static const char DBUS_DRIVER_XML[] =
" <interface name=\"org.freedesktop.DBus\">\n"
" <method name=\"Hello\">\n"
" <arg name=\"name\" type=\"s\" direction=\"out\"/>\n"
" </method>\n"
" <method name=\"AddMatch\">\n"
" <arg name=\"rule\" type=\"s\" direction=\"in\"/>\n"
" </method>\n"
" <method name=\"RemoveMatch\">\n"
" <arg name=\"rule\" type=\"s\" direction=\"in\"/>\n"
" </method>\n"
" </interface>\n";
/* Is `child` a path under `parent`? If so, write the first segment
@@ -219,6 +248,8 @@ static int handle_introspect(link_connection_t *conn, const struct link_msg *m)
"<node>\n");
xprintf(&x, "%s", STANDARD_INTERFACES_XML);
if (!strcmp(path, "/org/freedesktop/DBus"))
xprintf(&x, "%s", DBUS_DRIVER_XML);
o = NULL;
{
@@ -238,6 +269,7 @@ static int handle_introspect(link_connection_t *conn, const struct link_msg *m)
TAILQ_FOREACH(e, &o->vtables, link) {
const link_property_t *prop;
const link_signal_t *sig;
xprintf(&x, " <interface name=\"%s\">\n",
e->vt->interface);
@@ -247,6 +279,9 @@ static int handle_introspect(link_connection_t *conn, const struct link_msg *m)
if (e->vt->properties)
for (prop = e->vt->properties; prop->name; prop++)
emit_property(&x, prop);
if (e->vt->signals)
for (sig = e->vt->signals; sig->name; sig++)
emit_signal(&x, sig);
xprintf(&x, " </interface>\n");
}
}
+72 -9
View File
@@ -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)
{
@@ -287,6 +288,7 @@ int link_call_reply_error(link_call_t *call, const char *name, const char *messa
int link_call_read_byte (link_call_t *c, uint8_t *o) { return __r_byte (&c->read_cursor, o); }
int link_call_read_bool (link_call_t *c, int *o) { return __r_bool (&c->read_cursor, o); }
int link_call_read_u32 (link_call_t *c, uint32_t *o) { return __r_u32 (&c->read_cursor, o); }
int link_call_read_u64 (link_call_t *c, uint64_t *o) { return __r_u64 (&c->read_cursor, o); }
int link_call_read_string(link_call_t *c, const char **o) { return __r_string(&c->read_cursor, o); }
int link_call_read_path (link_call_t *c, const char **o) { return __r_path (&c->read_cursor, o); }
@@ -298,6 +300,7 @@ ssize_t link_writer_finish(link_writer_t *w) { return
void link_w_byte (link_writer_t *w, uint8_t v) { __w_byte(w, v); }
void link_w_bool (link_writer_t *w, int v) { __w_bool(w, v); }
void link_w_u32 (link_writer_t *w, uint32_t v) { __w_u32(w, v); }
void link_w_u64 (link_writer_t *w, uint64_t v) { __w_u64(w, v); }
void link_w_string (link_writer_t *w, const char *s) { __w_string(w, s); }
void link_w_path (link_writer_t *w, const char *s) { __w_path(w, s); }
void link_w_variant_string(link_writer_t *w, const char *s) { __w_variant_string(w, s); }
@@ -312,6 +315,7 @@ void link_reader_init(link_reader_t *r, const uint8_t *body, size_t len) { __r_i
int link_r_byte (link_reader_t *r, uint8_t *o) { return __r_byte (r, o); }
int link_r_bool (link_reader_t *r, int *o) { return __r_bool (r, o); }
int link_r_u32 (link_reader_t *r, uint32_t *o) { return __r_u32 (r, o); }
int link_r_u64 (link_reader_t *r, uint64_t *o) { return __r_u64 (r, o); }
int link_r_string(link_reader_t *r, const char **o) { return __r_string(r, o); }
int link_r_path (link_reader_t *r, const char **o) { return __r_path (r, o); }
int link_r_variant_begin (link_reader_t *r, char *type) { return __r_variant_begin(r, type); }
@@ -390,6 +394,7 @@ static struct link_parked *park(link_connection_t *conn, const uint8_t *frame,
bus->parked[i].tok = ++bus->next_tok;
bus->parked[i].conn = conn;
bus->parked[i].stamp = __now_ms();
bus->parked[i].uid = (uid_t)-1;
bus->parked[i].len = len;
memcpy(bus->parked[i].buf, frame, len);
*tok = bus->parked[i].tok;
@@ -474,13 +479,20 @@ void __dispatch_forget_conn(link_connection_t *conn)
static int dispatch_call(link_connection_t *conn, const struct link_msg *m,
const uint8_t *frame, size_t framelen,
const uid_t *known_uid);
const uid_t *known_uid, int resumed);
void link_uid_resolved(link_connection_t *conn, link_authz_t tok, uid_t uid)
/*
* Shared resume: copy the parked frame out, free the slot, re-enter
* dispatch. `uid` NULL means "use what the slot recorded" (a
* handler-parked call); non-NULL is a resolver's answer.
*/
static void resume_parked(link_connection_t *conn, link_authz_t tok,
const uid_t *uid, int resumed)
{
uint8_t buf[LINK_PARKED_MSG_MAX];
struct link_parked *p = NULL;
struct link_msg msg;
uid_t slot_uid;
size_t len;
int i;
@@ -498,15 +510,58 @@ void link_uid_resolved(link_connection_t *conn, link_authz_t tok, uid_t uid)
/* Copy the message out and free the slot before dispatching:
* the handler may park a call of its own. */
len = p->len;
len = p->len;
slot_uid = p->uid;
memcpy(buf, p->buf, len);
unpark(p);
if (__msg_parse(buf, len, &msg) <= 0)
return;
__dbg("resumed %s, caller uid %d", msg.member ? msg.member : "call", (int)uid);
(void)dispatch_call(conn, &msg, NULL, 0, &uid);
if (!uid)
uid = &slot_uid;
__dbg("resumed %s, caller uid %d", msg.member ? msg.member : "call", (int)*uid);
(void)dispatch_call(conn, &msg, NULL, 0, uid, resumed);
}
void link_uid_resolved(link_connection_t *conn, link_authz_t tok, uid_t uid)
{
resume_parked(conn, tok, &uid, 0);
}
/*
* Defer the reply: hold the request and re-run the handler later via
* link_call_resume(), where link_call_resumed() reads true. A resumed
* call cannot park again -- the raw frame is gone -- so the handler
* must answer on the second run. link_connection_expire() is the
* backstop for a resume that never comes.
*/
int link_call_park(link_call_t *call, link_authz_t *tok)
{
struct link_parked *p;
if (!call || !tok || call->resumed)
return -1;
p = park(call->conn, call->frame, call->framelen, tok);
if (!p)
return -1;
p->uid = call->uid;
call->parked = 1;
return 0;
}
void link_call_resume(link_connection_t *conn, link_authz_t tok)
{
resume_parked(conn, tok, NULL, 1);
}
int link_call_resumed(const link_call_t *call)
{
return call ? call->resumed : 0;
}
int __dispatch_message(link_connection_t *conn, const struct link_msg *m, size_t framelen)
@@ -521,7 +576,7 @@ int __dispatch_message(link_connection_t *conn, const struct link_msg *m, size_t
return 0;
}
return dispatch_call(conn, m, conn->rxbuf, framelen, NULL);
return dispatch_call(conn, m, conn->rxbuf, framelen, NULL, 0);
}
/* Who may invoke a privileged method. Without an authorizer, only
@@ -579,7 +634,7 @@ static int resolve_caller(link_connection_t *conn, const struct link_msg *m,
static int dispatch_call(link_connection_t *conn, const struct link_msg *m,
const uint8_t *frame, size_t framelen,
const uid_t *known_uid)
const uid_t *known_uid, int resumed)
{
struct link_object *o;
struct link_vtable_entry *e = NULL;
@@ -673,8 +728,8 @@ static int dispatch_call(link_connection_t *conn, const struct link_msg *m,
* resolved over a broker carries only its uid, so the
* authorizer sees no groups and falls back to root-only. */
if (!caller_may(conn->server, call_uid,
known_uid ? NULL : conn->peer_groups,
known_uid ? 0 : conn->peer_ngroups)) {
conn->broker ? NULL : conn->peer_groups,
conn->broker ? 0 : conn->peer_ngroups)) {
__dbg("denied %s, caller uid %d is not privileged",
m->member, (int)call_uid);
return __send_error(conn, m,
@@ -687,10 +742,18 @@ static int dispatch_call(link_connection_t *conn, const struct link_msg *m,
call.conn = conn;
call.uid = call_uid;
call.incoming = *m;
call.frame = frame;
call.framelen = framelen;
call.resumed = resumed;
__r_init(&call.read_cursor, m->body, m->body_avail);
rc = meth->handler(&call, e->userdata);
/* The handler parked the call: no reply yet, it is resumed via
* link_call_resume() or expired by link_connection_expire(). */
if (call.parked)
return 0;
/* Every path returns the status of whatever it put on the wire,
* so a failed send propagates out and the read loop drops the
* peer rather than leaving a half-written frame on a live link.
+6 -1
View File
@@ -40,7 +40,7 @@ typedef enum {
* than a number per call site. */
#define LINK_CALL_HDR_MAX 1024
#define LINK_CALL_BODY_MAX 1024
#define LINK_PARKED_CAP 4 /* inbound calls awaiting a uid */
#define LINK_PARKED_CAP 4 /* inbound calls held for later */
/* A call parked for authorization is a privileged one: an object path
* and at most a service name. Finit's per-service paths alone run to
* 512 bytes, so leave room for the header around one. Anything that
@@ -74,6 +74,7 @@ struct link_parked {
link_authz_t tok;
link_connection_t *conn;
uint64_t stamp;
uid_t uid; /* caller, for handler-parked calls */
size_t len;
uint8_t buf[LINK_PARKED_MSG_MAX];
};
@@ -133,6 +134,10 @@ struct link_call {
int reply_consumed;
int error_sent;
uid_t uid; /* caller, resolved for a broker peer */
const uint8_t *frame; /* raw frame, for link_call_park() */
size_t framelen;
int parked; /* handler deferred its reply */
int resumed; /* re-run via link_call_resume() */
};
/* A parsed AddMatch rule. Fields are NULL when the rule omits the
+27
View File
@@ -155,6 +155,19 @@ void link_server_set_authorizer(link_server_t *server, link_authorizer_t cb,
* asked about; a reply callback is handed it as its first argument. */
void link_uid_resolved(link_connection_t *conn, link_authz_t tok, uid_t uid);
/* ---------- handler-deferred replies ---------- */
/* A handler that cannot answer yet parks the call and returns 0
* without replying; the framework sends nothing. Resume re-runs the
* handler with link_call_resumed() reading true, and this time it
* must reply -- a resumed call cannot park again. Parked calls that
* are never resumed are expired by link_connection_expire() with a
* TimedOut error. Room is limited (four per connection); a failed
* park means answer now. */
int link_call_park (link_call_t *call, link_authz_t *tok);
void link_call_resume (link_connection_t *conn, link_authz_t tok);
int link_call_resumed(const link_call_t *call);
/* Called with the reply to an outbound link_connection_call(). `reply`
* is NULL if the connection dropped before one arrived. */
typedef void (*link_reply_cb_t)(link_connection_t *conn, const link_reply_t *reply,
@@ -247,10 +260,20 @@ typedef struct {
link_property_getter_fn getter;
} link_property_t;
/*
* Declares a signal for introspection only; emission is unchecked,
* see link_connection_emit_signal().
*/
typedef struct {
const char *name; /* member name */
const char *sig; /* D-Bus signature, e.g. "sss" */
} link_signal_t;
typedef struct {
const char *interface; /* e.g. "org.finit.Manager1" */
const link_method_t *methods; /* terminated by {NULL, ...}, or NULL */
const link_property_t *properties; /* terminated by {NULL, ...}, or NULL */
const link_signal_t *signals; /* terminated by {NULL, ...}, or NULL */
} link_vtable_t;
/* Register one (interface, methods) at `path`. Calling repeatedly
@@ -270,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 ----------
*
@@ -283,6 +307,7 @@ uid_t link_call_uid (const link_call_t *call);
int link_call_read_byte (link_call_t *call, uint8_t *out);
int link_call_read_bool (link_call_t *call, int *out);
int link_call_read_u32 (link_call_t *call, uint32_t *out);
int link_call_read_u64 (link_call_t *call, uint64_t *out);
int link_call_read_string(link_call_t *call, const char **out); /* "s" */
int link_call_read_path (link_call_t *call, const char **out); /* "o" */
@@ -424,6 +449,7 @@ ssize_t link_writer_finish(link_writer_t *w);
void link_w_byte (link_writer_t *w, uint8_t v);
void link_w_bool (link_writer_t *w, int v);
void link_w_u32 (link_writer_t *w, uint32_t v);
void link_w_u64 (link_writer_t *w, uint64_t v); /* "t" */
void link_w_string (link_writer_t *w, const char *s); /* "s" */
void link_w_path (link_writer_t *w, const char *s); /* "o" */
void link_w_variant_string(link_writer_t *w, const char *s); /* "v" containing "s" */
@@ -441,6 +467,7 @@ void link_reader_init(link_reader_t *r, const uint8_t *body, size_t len);
int link_r_byte (link_reader_t *r, uint8_t *out);
int link_r_bool (link_reader_t *r, int *out);
int link_r_u32 (link_reader_t *r, uint32_t *out);
int link_r_u64 (link_reader_t *r, uint64_t *out);
int link_r_string (link_reader_t *r, const char **out); /* "s" */
int link_r_path (link_reader_t *r, const char **out); /* "o" */
int link_r_variant_begin (link_reader_t *r, char *type); /* sig header, cursor at value */
+30
View File
@@ -79,6 +79,14 @@ void __w_u32(struct link_writer *w, uint32_t v)
put_u32(w, v);
}
void __w_u64(struct link_writer *w, uint64_t v)
{
if (reserve(w, 8, 8) < 0)
return;
put_u32(w, (uint32_t)(v & 0xffffffffu));
put_u32(w, (uint32_t)(v >> 32));
}
static void write_lenprefixed(struct link_writer *w, const char *s, int onebyte_len)
{
size_t len = s ? strlen(s) : 0;
@@ -230,6 +238,18 @@ int __r_u32(struct link_reader *r, uint32_t *out)
return 0;
}
int __r_u64(struct link_reader *r, uint64_t *out)
{
if (r_skip_align(r, 8) < 0 || r->off + 8 > r->cap) {
r->err = 1;
return -1;
}
*out = (uint64_t)rd_u32(r->base + r->off)
| ((uint64_t)rd_u32(r->base + r->off + 4) << 32);
r->off += 8;
return 0;
}
int __r_bool(struct link_reader *r, int *out)
{
uint32_t v;
@@ -327,6 +347,13 @@ int __r_skip_basic(struct link_reader *r, char type)
case 'u':
case 'i':
return __r_u32(r, &u);
case 't':
case 'x':
case 'd': {
uint64_t t;
return __r_u64(r, &t);
}
case 'y':
return __r_byte(r, &y);
default:
@@ -396,6 +423,9 @@ ssize_t __marshal_va(uint8_t *body, size_t cap, const char *sig, va_list ap)
case 'u':
__w_u32(&w, va_arg(ap, uint32_t));
break;
case 't':
__w_u64(&w, va_arg(ap, uint64_t));
break;
case 's':
__w_string(&w, va_arg(ap, const char *));
break;
+2
View File
@@ -22,6 +22,7 @@ ssize_t __w_finish(struct link_writer *w);
void __w_byte (struct link_writer *w, uint8_t v);
void __w_bool (struct link_writer *w, int v);
void __w_u32 (struct link_writer *w, uint32_t v);
void __w_u64 (struct link_writer *w, uint64_t v);
void __w_string (struct link_writer *w, const char *s); /* "s" */
void __w_path (struct link_writer *w, const char *s); /* "o" */
void __w_sig (struct link_writer *w, const char *s); /* "g" */
@@ -44,6 +45,7 @@ void __r_init (struct link_reader *r, const uint8_t *body, size_t len);
int __r_byte (struct link_reader *r, uint8_t *out);
int __r_bool (struct link_reader *r, int *out);
int __r_u32 (struct link_reader *r, uint32_t *out);
int __r_u64 (struct link_reader *r, uint64_t *out);
int __r_string(struct link_reader *r, const char **out); /* "s" */
int __r_path (struct link_reader *r, const char **out); /* "o" */
int __r_variant_begin (struct link_reader *r, char *type); /* sig header, cursor at value */
+4
View File
@@ -1,2 +1,6 @@
dist_man8_MANS = finit.8 initctl.8
dist_man5_MANS = finit.conf.5
if KEVENTD
dist_man8_MANS += keventd.8
endif
+8
View File
@@ -19,6 +19,10 @@ is the official tool for interacting with
.Xr finit 8 .
It comes with functions for showing status, querying health of services,
as well as editing, enabling and disabling services.
Commands are sent over the Finit D-Bus API when available, with the
legacy
.Pa INIT_SOCKET
protocol as fallback.
.Sh OPTIONS
This program follows the usual UNIX command line syntax, with long
options starting with two dashes (`-'). The options are as follows:
@@ -205,6 +209,10 @@ restricted
Send signal S to service, by name.
.Cm S
may be a complete signal name such as SIGHUP, or short name such as HUP, or the signal number such as 1 (SIGHUP)
.It Nm Ar monitor
Stream service state, runlevel, and condition change signals from the
Finit D-Bus API until interrupted. Only available when Finit is built
with D-Bus support
.It Nm Ar restart Cm NAME[:ID]
Restart (stop/start) service by name
.It Nm Ar status Cm NAME[:ID]
+368
View File
@@ -0,0 +1,368 @@
.\" Hey, EMACS: -*- nroff -*-
.\" First parameter, NAME, should be all caps
.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection
.\" other parameters are allowed: see man(7), man(1)
.Dd Aug 16, 2026
.\" Please adjust this date whenever revising the manpage.
.Dt KEVENTD 8 SMM
.Os Linux
.Sh NAME
.Nm keventd
.Nd Finit device manager daemon
.Sh SYNOPSIS
.Nm
.Op Fl c
.Op Fl d
.Op Fl g Ar group
.Op Fl G
.Op Fl h
.Op Fl n
.Op Fl p
.Op Fl r Ar dir
.Op Fl S
.Op Fl t Ar sec
.Op Fl v
.Sh DESCRIPTION
.Nm
is a built-in device manager bundled with
.Xr finit 8 .
It listens for kernel uevents on a
.Dv NETLINK_KOBJECT_UEVENT
socket and handles device node creation, persistent symlinks, firmware
loading, kernel module loading, and condition management. Events are
matched against udev rules read from
.Pa /lib/udev/rules.d ,
.Pa /run/udev/rules.d ,
and
.Pa /etc/udev/rules.d ,
and device properties are persisted to
.Pa /run/udev/data
in the format libudev consumers expect.
.Pp
It replaces the need for external device managers like
.Nm mdev ,
.Nm mdevd ,
or
.Nm udevd
on systems where a lighter-weight solution is preferred, particularly
on embedded systems.
.Sh OPTIONS
.Bl -tag -width Ds
.It Fl c
Run coldplug at startup. Walks the
.Pa /sys/devices
tree and writes
.Cm add
to each
.Pa uevent
file, causing the kernel to re-emit add events for all devices already
present. This populates
.Pa /dev
with nodes for hardware that existed before
.Nm
started.
.It Fl d
Enable debug mode. Implies
.Fl n .
All received uevents are logged to stderr.
.It Fl h
Show help text and exit.
.It Fl g Ar group
Override the netlink multicast group mask used for uevent rebroadcast.
The default is 4 (group 0x4), which is the
.Sy libudev-zero
convention.
Bit 0 is always masked out to prevent a feedback loop with the kernel
group.
See
.Sx NETLINK REBROADCAST
below.
.It Fl G
Disable netlink uevent rebroadcast entirely.
See
.Sx NETLINK REBROADCAST
below.
.It Fl n
Run in foreground, do not daemonize. Log messages are written to
stderr instead of syslog.
.It Fl p
Passive mode: monitor power supply events only, leaving device
management to an external device manager.
.It Fl r Ar dir
Extra udev rules directory, searched after the standard paths.
.It Fl S
Settle: wait until the device event queue is quiet, then exit 0, or
non-zero on timeout. Asks the running
.Nm
over D-Bus when available, else polls the kernel's uevent sequence
counter.
.It Fl t Ar sec
Settle timeout in seconds, default 30. Only with
.Fl S .
.It Fl v
Show version and exit.
.El
.Sh DEVICE NODES
On receiving an
.Cm add
event with
.Cm MAJOR ,
.Cm MINOR ,
and
.Cm DEVNAME
fields,
.Nm
creates the device node in
.Pa /dev
using
.Xr mknod 2 .
Parent directories are created as needed, e.g.\&
.Pa /dev/input/
for
.Pa /dev/input/event0 .
.Pp
On
.Cm remove
events the device node and its associated symlinks are cleaned up.
.Pp
Permissions are assigned based on built-in rules matching on device
subsystem and name. For example, block devices default to mode 0660
owned by root:disk, TTY devices to root:tty, and common devices like
.Pa /dev/null
and
.Pa /dev/zero
are world-readable (0666).
.Sh PERSISTENT SYMLINKS
For block devices,
.Nm
creates symlinks in
.Pa /dev/disk/ :
.Bl -tag -width by-path -offset indent -compact
.It Pa by-id
Based on device serial number and model, read from sysfs.
.It Pa by-path
Based on the device topology path.
.El
.Pp
For input devices, symlinks are created in
.Pa /dev/input/ :
.Bl -tag -width by-path -offset indent -compact
.It Pa by-id
Based on the device name from sysfs.
.It Pa by-path
Based on the physical device path.
.El
.Pp
Symlinks are tracked internally and automatically removed when the
device is unplugged.
.Sh FIRMWARE LOADING
When a kernel driver requests firmware via
.Fn request_firmware ,
the kernel emits a uevent with a
.Cm FIRMWARE
field.
.Nm
handles this by searching for the firmware file in the following order:
.Pp
.Bl -enum -compact -offset indent
.It
.Pa /lib/firmware/updates/<kernel-version>/<name>
.It
.Pa /lib/firmware/updates/<name>
.It
.Pa /lib/firmware/<kernel-version>/<name>
.It
.Pa /lib/firmware/<name>
.El
.Pp
The firmware is loaded by writing to the device's sysfs
.Pa loading
and
.Pa data
attributes.
.Sh MODULE LOADING
When a device add event includes a
.Cm MODALIAS
field,
.Nm
spawns
.Cm modprobe -bq
to load the matching kernel module. Module loading is asynchronous
to avoid blocking event processing.
.Sh CONDITIONS
.Nm
provides conditions for Finit's dependency system by creating and
removing symlinks in
.Pa /run/finit/cond/ .
.Ss Device Conditions
When a device node is created,
.Nm
asserts a corresponding
.Cm dev/
condition. For example, creating
.Pa /dev/sda
asserts
.Cm dev/sda .
This allows services to depend on specific devices:
.Bd -literal -offset indent
service mdadm {
description = "RAID monitor"
runlevel = "2345"
conditions = { "dev/sda" }
command = "/usr/sbin/mdadm"
}
service gpsd {
description = "GPS daemon"
runlevel = "2345"
conditions = { "dev/ttyUSB0" }
command = "/usr/sbin/gpsd"
}
.Ed
.Pp
When the device is removed, the condition is cleared and Finit stops
the dependent services.
.Ss Power Supply Conditions
.Nm
monitors the
.Cm power_supply
subsystem and provides:
.Bl -tag -width sys/pwr/ac -offset indent -compact
.It Cm sys/pwr/ac
Asserted when AC power is connected.
.El
.Pp
Useful for preventing power-hungry services from running on battery:
.Bd -literal -offset indent
service cron {
description = "Cron daemon"
runlevel = "2345"
conditions = { "sys/pwr/ac" }
command = "cron -f"
}
.Ed
.Ss Class and Driver Conditions
Devices without a
.Pa /dev
node are covered by
.Cm class/<subsystem>/<name> ,
asserted on every sysfs class device add, e.g.\&
.Cm class/leds/blue
or
.Cm class/net/eth0 .
.Cm driver/<name>
is asserted while the driver
.Ar name
is bound to at least one device, from the kernel's bind/unbind
uevents, e.g.\&
.Cm driver/mt7530 .
.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 .
Only the device manager listens on this raw kernel group. Userspace
consumers such as applications using
.Sy libudev
expect to receive processed events on a separate netlink group.
.Pp
.Sy systemd/udevd
established the convention of rebroadcasting to a separate group, and
.Sy libudev-zero ,
a daemonless replacement for libudev, listens on group 0x4 for these
events. Without rebroadcast, graphical applications, Wayland and X11
compositors, libinput, and other libudev consumers will not receive
device hotplug events.
.Pp
.Nm
rebroadcasts by default to netlink group 4 (bit 2, i.e.\&
.Li 0x4 ) .
A second netlink socket is created at startup, and after each uevent
has been fully processed (device nodes created, modules loaded,
etc.\&), the original event is sent to the configured group. This
ensures that device nodes and symlinks already exist by the time
consumers receive the event.
.Pp
Use
.Fl g
to override the default group mask, or
.Fl G
to disable rebroadcast entirely.
.Pp
See also:
.Lk https://github.com/illiliti/libudev-zero "libudev-zero" ,
.Lk https://skarnet.org/software/mdevd/ "mdevd" .
.Sh SIGNALS
.Bl -tag -width SIGUSR1
.It Dv SIGHUP
Re-read the udev rules directories.
.It Dv SIGUSR1
Toggle debug logging at runtime.
.It Dv SIGTERM
Graceful shutdown.
.El
.Sh FILES
.Bl -tag -width /run/finit/cond/dev/ -compact
.It Pa /dev/
Device nodes managed by
.Nm .
.It Pa /dev/disk/by-id/ , Pa /dev/disk/by-path/
Persistent block device symlinks.
.It Pa /dev/input/by-id/ , Pa /dev/input/by-path/
Persistent input device symlinks.
.It Pa /lib/firmware/
Firmware search path.
.It Pa /run/finit/cond/dev/
Device condition symlinks, also
.Pa class/
and
.Pa driver/ .
.It Pa /run/finit/cond/sys/pwr/
Power supply condition symlinks.
.It Pa /lib/udev/rules.d/ , Pa /run/udev/rules.d/ , Pa /etc/udev/rules.d/
udev rules, searched in this order.
.It Pa /run/udev/data/
Per-device property database, libudev compatible.
.It Pa /run/keventd/bus
The
.Cm org.finit.Device1
D-Bus socket.
.El
.Sh SEE ALSO
.Xr finit 8 ,
.Xr finit.conf 5 ,
.Xr initctl 8 ,
.Xr mknod 2 ,
.Xr modprobe 8
.Sh AUTHORS
.An Joachim Wiberg Aq Mt troglobit@gmail.com
+1
View File
@@ -56,6 +56,7 @@ nav:
- Plugins: plugins.md
- Watchdog: watchdog.md
- keventd: keventd.md
- udev Rule Matching: udev-matching.md
- D-Bus Integration: dbus.md
- Service State Machine: state-machine.md
- Distributions: distro.md
+2 -9
View File
@@ -13,9 +13,6 @@ pkglibexec_PROGRAMS = getty logit runparts tmpfiles
if SULOGIN
pkglibexec_PROGRAMS += sulogin
endif
if KEVENTD
pkglibexec_PROGRAMS += keventd
endif
if WATCHDOGD
pkglibexec_PROGRAMS += watchdogd
endif
@@ -30,11 +27,6 @@ getty_CFLAGS = -W -Wall -Wextra -std=gnu99
getty_CFLAGS += $(lite_CFLAGS)
getty_LDADD = $(lite_LIBS)
keventd_SOURCES = keventd.c iwatch.c iwatch.h util.c util.h
keventd_CFLAGS = -W -Wall -Wextra -std=gnu99
keventd_CFLAGS += $(lite_CFLAGS)
keventd_LDADD = $(lite_LIBS)
runparts_SOURCES = runparts.c
runparts_CFLAGS = -W -Wall -Wextra -std=gnu99
runparts_CFLAGS += $(lite_CFLAGS)
@@ -105,11 +97,12 @@ endif
initctl_SOURCES = initctl.c initctl.h cgutil.c cgutil.h \
client.c client.h cond.c cond.h reboot.c \
serv.c serv.h svc.h util.c util.h log.h
initctl_CPPFLAGS = $(AM_CPPFLAGS)
initctl_CFLAGS = -W -Wall -Wextra -Wno-unused-parameter -std=gnu99
initctl_CFLAGS += $(lite_CFLAGS) $(uev_CFLAGS)
initctl_LDADD = $(lite_LIBS) $(uev_LIBS)
if DBUS
initctl_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/libink
initctl_CPPFLAGS += -I$(top_srcdir)/libink
initctl_LDADD += $(top_builddir)/libink/libink.la
endif
+7 -71
View File
@@ -61,53 +61,19 @@ static int call(int (*action)(svc_t *, void *), char *buf, size_t len)
static int stop(svc_t *svc, void *user_data)
{
(void)user_data;
if (!svc)
return 1;
service_timeout_cancel(svc);
svc_stop(svc);
service_step(svc);
if (!IS_RESERVED_RUNLEVEL(runlevel))
service_step_all(SVC_TYPE_ANY);
return 0;
return service_stop_now(svc);
}
static int start(svc_t *svc, void *user_data)
{
(void)user_data;
if (!svc)
return 1;
service_timeout_cancel(svc);
svc_start(svc);
service_step(svc);
if (!IS_RESERVED_RUNLEVEL(runlevel))
service_step_all(SVC_TYPE_ANY);
return 0;
return service_start_now(svc);
}
/*
* NOTE: this does not wait for svc to be stopped first, that is the
* responsibility of initctl to do. Otherwise we'd block PID 1,
* or introduce some nasty race conditions.
*/
static int restart(svc_t *svc, void *user_data)
{
if (!svc)
return 1;
if (!svc_is_running(svc))
return start(svc, user_data);
service_timeout_cancel(svc);
service_stop(svc);
service_step(svc);
return 0;
(void)user_data;
return service_restart_now(svc);
}
static int reload(svc_t *svc, void *user_data)
@@ -222,16 +188,7 @@ static svc_t *do_find_byc(char *buf, size_t len)
return svc_find_by_cond(input);
}
static void bypass_shutdown(void *);
struct wq emergency = { .cb = bypass_shutdown };
static void bypass_shutdown(void *unused)
{
(void)unused;
cprintf("TIMEOUT TIMEOUT SHUTTING DOWN NOW!!\n");
do_shutdown(halt);
}
/*
* Handle switch_root API command.
@@ -302,10 +259,7 @@ static int do_reboot(int cmd, int timeout, char *buf, size_t len)
return 255;
}
if (timeout > 0) {
emergency.delay = timeout * 1000;
schedule_work(&emergency);
}
shutdown_bypass(timeout);
switch (cmd) {
case INIT_CMD_REBOOT:
@@ -375,8 +329,8 @@ static void api_cb(uev_t *w, void *arg, int events)
{
static svc_t *iter = NULL;
struct init_request rq;
int sd, lvl;
svc_t *svc;
int sd;
(void)arg;
@@ -454,14 +408,6 @@ static void api_cb(uev_t *w, void *arg, int events)
switch (rq.cmd) {
case INIT_CMD_RUNLVL:
/* Allow changing cfglevel in runlevel S */
if (IS_RESERVED_RUNLEVEL(runlevel)) {
if (runlevel != INIT_LEVEL) {
warnx("Cannot abort runlevel 6/0.");
break;
}
}
switch (rq.runlevel) {
case 's':
case 'S':
@@ -470,17 +416,7 @@ static void api_cb(uev_t *w, void *arg, int events)
case '0'...'9':
dbg("Setting new runlevel %c", rq.runlevel);
lvl = rq.runlevel - '0';
if (lvl == 0)
halt = SHUT_OFF;
if (lvl == 6)
halt = SHUT_REBOOT;
/* User requested change in next runlevel */
if (runlevel == INIT_LEVEL)
cfglevel = lvl;
else
sm_runlevel(lvl);
sm_request_runlevel(rq.runlevel - '0');
break;
default:
+4
View File
@@ -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
+31 -28
View File
@@ -701,9 +701,9 @@ char *conf_escape(const char *str, char *buf, size_t len)
}
/*
* Called by plugins and similar that dynamically generate system
* services. @body holds the block contents, one indented line per
* setting.
* Generates /run/finit/system/<file> for Finit's built-in services,
* which must probe the running system before registering. @fmt holds
* the block contents, one indented line per setting.
*/
void conf_save_service(int type, const char *name, char *file, const char *fmt, ...)
{
@@ -2307,28 +2307,23 @@ int conf_reload(void)
}
/*
* Next, read all *.conf in /lib/finit/system and /etc/finit.d/
* The system files were previously created at runtime by plugins
* but are now regular files that can be overridden by files in
* /etc/finit.d -- similar to how tmfiles.d(5) work. E.g., add
* an override .conf, or an ignore by symlinking to /dev/null
* Next, read all *.conf in /run/finit/system, /lib/finit/system,
* and /etc/finit.d/. The runtime directory holds files Finit
* generates for its built-in services (keventd, watchdogd,
* runparts), read first so later files can reference them in
* if: statements. A same-named file in a later directory
* overrides an earlier one, similar to how tmpfiles.d(5) work,
* so /etc/finit.d, always read last, is how users override any
* built-in or bundled configuration.
*
* The .conf files (and run/task/service stanzas) are parsed and
* started in order. Each directory is sorted alphanumerically
* and then the result is appended to the overall order:
*
* /lib/finit/system/10-hotplug.conf
* /lib/finit/system/90-testserv.conf
* /run/finit/system/dbus.conf
* /run/finit/system/tty.conf
* /etc/finit.d/10-abc.conf
* /etc/finit.d/20-abc.conf
* /etc/finit.d/enabled/1-aaa.conf
* /etc/finit.d/enabled/1-abc.conf
* /etc/finit.d/enabled/2-aaa.conf
* The .conf files (and their run/task/service stanzas) are
* parsed and started in order. Each directory is sorted
* alphanumerically before being appended to the overall order,
* recorded in /run/finit/conf.order at bootstrap. See
* doc/config/files.md for an example.
*/
glob_append(&gl, 0, "%s/*.conf", FINIT_SYSPATH_);
glob_append(&gl, 1, "%s/*.conf", FINIT_RUNPATH_);
glob_append(&gl, 0, "%s/*.conf", FINIT_RUNPATH_);
glob_append(&gl, 1, "%s/*.conf", FINIT_SYSPATH_);
glob_append(&gl, 1, "%s/*.conf", finit_rcsd);
glob_append(&gl, 1, "%s/enabled/*.conf", finit_rcsd);
@@ -2683,15 +2678,23 @@ int conf_init(uev_ctx_t *ctx)
}
#endif
/*
* Start kernel event daemon as soon as possible, if enabled
* Start kernel event daemon as soon as possible, if enabled.
* In passive mode (-p) alongside the hotplug plugin, keventd
* only monitors power supply events (sys/pwr/ac condition).
*/
if (whichp(FINIT_EXECPATH_ "/keventd"))
conf_save_service(SVC_TYPE_SERVICE, "keventd", "keventd.conf",
"\tdescription = \"Finit kernel event daemon\"\n"
"\trunlevel = \"S12345789\"\n"
"\tnotify = \"none\"\n"
"\tdescription = \"kernel event daemon\"\n"
"\trunlevel = \"S0123456789\"\n"
"\tnotify = \"pid\"\n"
"\tcgroup init {}\n"
"\tcommand = \"" FINIT_EXECPATH_ "/keventd\"\n");
"\tcommand = \"" FINIT_EXECPATH_ "/keventd"
#ifdef HAVE_HOTPLUG_PLUGIN
" -p"
#else
" -c"
#endif
"\"\n");
dbg("Allow plugins to register early runlevel 1 run/task/services ...");
plugin_run_hooks(HOOK_SVC_PLUGIN);
+71 -50
View File
@@ -314,46 +314,24 @@ static int manager_get_service(link_call_t *call, void *userdata)
return 0;
}
/* Service-control helpers used by Start/Stop/Restart/Reload. These
* mirror the static helpers in api.c — kept private here so api.c
* stays untouched in this increment. */
/* svc_parse_jobstr-style adapters over the shared service helpers */
static int dbus_apply_stop(svc_t *svc, void *user_data)
{
(void)user_data;
if (!svc)
return 1;
service_timeout_cancel(svc);
svc_stop(svc);
service_step(svc);
if (!IS_RESERVED_RUNLEVEL(runlevel))
service_step_all(SVC_TYPE_ANY);
return 0;
return service_stop_now(svc);
}
static int dbus_apply_start(svc_t *svc, void *user_data)
{
(void)user_data;
if (!svc)
return 1;
service_timeout_cancel(svc);
svc_start(svc);
service_step(svc);
if (!IS_RESERVED_RUNLEVEL(runlevel))
service_step_all(SVC_TYPE_ANY);
return 0;
return service_start_now(svc);
}
static int dbus_apply_restart(svc_t *svc, void *user_data)
{
if (!svc)
return 1;
if (!svc_is_running(svc))
return dbus_apply_start(svc, user_data);
service_timeout_cancel(svc);
service_stop(svc);
service_step(svc);
return 0;
(void)user_data;
return service_restart_now(svc);
}
struct dispatch_ctx {
@@ -454,9 +432,7 @@ static int manager_set_runlevel(link_call_t *call, void *userdata)
"org.freedesktop.DBus.Error.InvalidArgs",
"runlevel must be 0-9 (excluding internal levels)");
if (lvl == 0) halt = SHUT_OFF;
if (lvl == 6) halt = SHUT_REBOOT;
sm_runlevel((int)lvl);
sm_request_runlevel((int)lvl);
(void)link_call_reply(call);
return 0;
@@ -464,11 +440,18 @@ static int manager_set_runlevel(link_call_t *call, void *userdata)
static int dbus_shutdown(link_call_t *call, shutop_t target, int level)
{
uint32_t timeout;
if (link_call_read_u32(call, &timeout) < 0)
return link_call_reply_error(call,
"org.freedesktop.DBus.Error.InvalidArgs",
"expected (u)");
if (IS_RESERVED_RUNLEVEL(runlevel))
return link_call_reply_error(call,
"org.finit.Error.WrongRunlevel",
"Already in shutdown");
halt = target;
shutdown_bypass((int)timeout);
sm_runlevel(level);
(void)link_call_reply(call);
return 0;
@@ -490,11 +473,9 @@ static int signal_one(svc_t *svc, void *udata)
{
int signo = *(int *)udata;
/* Silently skip stopped services -- a multi-match ident
* (e.g. "sshd:*") should not fail the whole call just because
* one of the matches happens to be in a halted state. */
/* Signalling a stopped service is an error, like the legacy API */
if (!svc_is_running(svc))
return 0;
return 1;
return !!kill(svc->pid, signo);
}
@@ -535,6 +516,10 @@ static int manager_signal(link_call_t *call, void *u)
static int manager_suspend(link_call_t *call, void *u)
{
(void)u;
if (IS_RESERVED_RUNLEVEL(runlevel))
return link_call_reply_error(call,
"org.finit.Error.WrongRunlevel",
"Unsupported command in runlevel S and 6/0");
sync();
if (suspend() < 0) {
const char *msg = (errno == EINVAL)
@@ -623,11 +608,11 @@ static const link_method_t manager_methods[] = {
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_reload },
{ .name = "SetRunlevel", .in_sig = "u", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_set_runlevel },
{ .name = "Reboot", .in_sig = "", .out_sig = "",
{ .name = "Reboot", .in_sig = "u", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_reboot },
{ .name = "Poweroff", .in_sig = "", .out_sig = "",
{ .name = "Poweroff", .in_sig = "u", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_poweroff },
{ .name = "Halt", .in_sig = "", .out_sig = "",
{ .name = "Halt", .in_sig = "u", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_halt },
{ .name = "Suspend", .in_sig = "", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = manager_suspend },
@@ -638,10 +623,18 @@ static const link_method_t manager_methods[] = {
{ NULL, NULL, NULL, 0, NULL }
};
static const link_signal_t manager_signals[] = {
{ .name = "ServiceStateChanged", .sig = "sss" },
{ .name = "RunlevelChanged", .sig = "ss" },
{ .name = "ConfigReloaded", .sig = "" },
{ NULL, NULL }
};
static const link_vtable_t manager_vtable = {
.interface = "org.finit.Manager1",
.methods = manager_methods,
.properties = manager_properties,
.signals = manager_signals,
};
/* ---------- org.finit.Service1 (one object per service) ----------
@@ -665,7 +658,11 @@ static int service_action_method(link_call_t *call, void *userdata,
"org.finit.Error.NoSuchService",
"Service object no longer valid");
action(svc, NULL);
if (action(svc, NULL))
return link_call_reply_error(call,
"org.finit.Error.Failed",
"failed on service");
(void)link_call_reply(call);
return 0;
}
@@ -674,18 +671,15 @@ static int service1_start (link_call_t *c, void *u) { return service_action_met
static int service1_stop (link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_stop); }
static int service1_restart(link_call_t *c, void *u) { return service_action_method(c, u, dbus_apply_restart); }
static int service1_reload(link_call_t *call, void *userdata)
static int dbus_apply_reload(svc_t *svc, void *user_data)
{
svc_t *svc = userdata;
(void)user_data;
return service_reload(svc);
}
if (!svc)
return link_call_reply_error(call,
"org.finit.Error.NoSuchService",
"Service object no longer valid");
service_reload(svc);
(void)link_call_reply(call);
return 0;
static int service1_reload(link_call_t *c, void *u)
{
return service_action_method(c, u, dbus_apply_reload);
}
static const link_method_t service_methods[] = {
@@ -1049,6 +1043,18 @@ void dbus_notify_runlevel_change(int old_level, int new_level)
"RunlevelChanged", "ss", body, (size_t)blen);
}
/*
* Reconfiguration complete: all conditions have been re-asserted by
* their in-Finit owners. External providers whose conditions are
* generation files, rather than the oneshot symlinks keventd uses,
* subscribe to this to re-assert theirs.
*/
void dbus_notify_reload(void)
{
dbus_emit_signal("/org/finit/manager", "org.finit.Manager1",
"ConfigReloaded", "", NULL, 0);
}
/* ---------- org.finit.Cond1 ---------- */
#define COND_PATH_OBJECT "/org/finit/cond"
@@ -1148,7 +1154,7 @@ static int cond1_set_or_clear(link_call_t *call, int do_set)
"org.freedesktop.DBus.Error.InvalidArgs",
"Set/Clear is restricted to usr/* conditions");
if (do_set)
if (do_set) {
/* cond_set_oneshot, not cond_set: a user-asserted condition
* is a symlink to _PATH_RECONF, so it tracks the reconf
* generation automatically and stays "on" across reloads
@@ -1157,8 +1163,17 @@ static int cond1_set_or_clear(link_call_t *call, int do_set)
* semantics for user conditions, and what initctl cond set
* has done forever via the filesystem path. */
cond_set_oneshot(full);
else
if (cond_get(full) != COND_ON)
return link_call_reply_error(call,
"org.finit.Error.Failed",
"failed asserting condition");
} else {
cond_clear(full);
if (cond_get(full) != COND_OFF)
return link_call_reply_error(call,
"org.finit.Error.Failed",
"failed clearing condition");
}
(void)link_call_reply(call);
return 0;
@@ -1256,9 +1271,15 @@ static const link_method_t cond_methods[] = {
{ NULL, NULL, NULL, 0, NULL }
};
static const link_signal_t cond_signals[] = {
{ .name = "ConditionChanged", .sig = "ss" },
{ NULL, NULL }
};
static const link_vtable_t cond_vtable = {
.interface = COND_INTERFACE,
.methods = cond_methods,
.signals = cond_signals,
};
/* ---------- signal emission: ConditionChanged ---------- */
+52 -13
View File
@@ -58,6 +58,30 @@ static struct dev_node *find_node(const char *cond)
return NULL;
}
static int devmon_cond(const char *cond)
{
return !strncmp(cond, COND_DEV, strlen(COND_DEV)) ||
!strncmp(cond, COND_CLASS, strlen(COND_CLASS)) ||
!strncmp(cond, COND_DRIVER, strlen(COND_DRIVER));
}
/*
* keventd asserts class/ and driver/ conditions for devices that never
* get a /dev node, e.g. LEDs and DSA switch ports. For those the
* condition file is the only record of the device.
*/
static int dev_exists(const char *cond)
{
char path[PATH_MAX], tmp[PATH_MAX];
snprintf(path, sizeof(path), "/%s", cond);
if (fexist(path))
return 1;
snprintf(tmp, sizeof(tmp), _PATH_COND "%s", cond);
return fexist(pid_runpath(tmp, path, sizeof(path)));
}
static void drop_node(struct dev_node *node)
{
if (!node)
@@ -75,9 +99,8 @@ static void drop_node(struct dev_node *node)
void devmon_add_cond(const char *cond)
{
struct dev_node *node;
char path[PATH_MAX];
if (!cond || strncmp(cond, "dev/", 4)) {
if (!cond || !devmon_cond(cond)) {
// dbg("no match %s", cond ?: "<NIL>");
return;
}
@@ -105,14 +128,13 @@ void devmon_add_cond(const char *cond)
TAILQ_INSERT_TAIL(&dev_node_list, node, link);
snprintf(path, sizeof(path), "/%s", cond);
if (fexist(path))
if (dev_exists(cond))
cond_set(cond);
}
void devmon_del_cond(const char *cond)
{
if (!cond || strncmp(cond, COND_DEV, strlen(COND_DEV)))
if (!cond || !devmon_cond(cond))
return;
drop_node(find_node(cond));
@@ -121,11 +143,9 @@ void devmon_del_cond(const char *cond)
void devmon_reconf(void)
{
struct dev_node *node, *tmp;
char path[PATH_MAX];
TAILQ_FOREACH_SAFE(node, &dev_node_list, link, tmp) {
snprintf(path, sizeof(path), "/%s", node->name);
if (fexist(path))
if (dev_exists(node->name))
cond_set(node->name);
else
cond_clear(node->name);
@@ -161,6 +181,18 @@ static void devmon_update_conds(char *dir, char *name, uint32_t mask)
char *cond;
paste(fn, sizeof(fn), dir, name);
/*
* Events in the condition tree come from keventd, the file is
* the condition state so only kick affected services, like the
* sys/usr plugins do.
*/
cond = strstr(fn, COND_BASE);
if (cond) {
cond_update(cond + strlen(COND_BASE) + 1);
return;
}
cond = &fn[1];
// dbg("path: %s, mask: %08x, cond: %s", fn, mask, cond);
@@ -273,23 +305,30 @@ static void devmon_cb(uev_t *w, void *arg, int events)
void devmon_init(uev_ctx_t *ctx)
{
const char *cond_dirs[] = { _PATH_CONDDEV, _PATH_CONDCLASS, _PATH_CONDDRIVER };
char dir[MAX_ARG_LEN];
size_t i;
fd = iwatch_init(&iw_devmon);
if (fd < 0)
return;
if (mkpath(pid_runpath(_PATH_CONDDEV, dir, sizeof(dir)), 0755) && errno != EEXIST) {
err(1, "Failed creating %s condition directory, %s", COND_DEV, _PATH_CONDDEV);
return;
}
if (uev_io_init(ctx, &devw, devmon_cb, NULL, fd, UEV_READ)) {
err(1, "Failed setting up I/O callback for /dev watcher");
close(fd);
return;
}
for (i = 0; i < NELEMS(cond_dirs); i++) {
pid_runpath(cond_dirs[i], dir, sizeof(dir));
if (mkpath(dir, 0755) && errno != EEXIST) {
err(1, "Failed creating condition directory %s", dir);
continue;
}
devmon_add_path(&iw_devmon, dir);
}
strlcpy(dir, _PATH_DEV, sizeof(dir));
if (devmon_add_path(&iw_devmon, dir))
iwatch_exit(&iw_devmon);
+16 -3
View File
@@ -1067,13 +1067,26 @@ static int do_cmd(int cmd)
#ifdef HAVE_DBUS
static int do_reboot_dbus(const char *method)
{
int rc = try_dbus_manager(method, "", NULL);
link_client_t *c;
int rc;
if (rc == 0) {
c = link_client_open(FINIT_BUS_SOCKET);
if (!c)
return -1; /* fall back to legacy transport */
/* Timeout in seconds arms the emergency shutdown bypass, 0 = none */
rc = link_client_call_v(c, "/org/finit/manager",
"org.finit.Manager1", method,
"u", (uint32_t)timeout);
if (rc == LINK_CALL_ERROR)
map_dbus_err(c, method, NULL); /* exits */
link_client_close(c);
if (rc == LINK_CALL_OK) {
sleep(5); /* match legacy: wait for finit to shut down */
return 0;
}
return rc; /* 1 = error, -1 = fall back */
return -1; /* fall back */
}
#endif
-319
View File
@@ -1,319 +0,0 @@
/* Listens to kernel events like AC power status and manages sys/ conditions
*
* 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 <errno.h>
#include <dirent.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/types.h>
#include <linux/netlink.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
# include <lite/lite.h>
#endif
#include "cond.h"
#include "pid.h"
#include "util.h"
#define _PATH_SYSFS_PWR "/sys/class/power_supply"
static int num_ac_online;
static int num_ac;
static int running = 1;
static int level;
static int logon;
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)); }
static void sys_cond(char *cond, int set)
{
char oneshot[256];
snprintf(oneshot, sizeof(oneshot), "%s/%s", _PATH_CONDSYS, cond);
if (set) {
if (symlink(_PATH_RECONF, oneshot) && errno != EEXIST)
warn("failed asserting sys/%s", cond);
} else {
if (erase(oneshot) && errno != ENOENT)
warn("failed asserting sys/%s", cond);
}
}
static int fgetline(char *path, char *buf, size_t len)
{
FILE *fp;
fp = fopen(path, "r");
if (!fp)
return -1;
if (!fgets(buf, len, fp)) {
fclose(fp);
return -1;
}
chomp(buf);
fclose(fp);
return 0;
}
static int check_online(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(char *type)
{
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;
}
static void init(void)
{
struct dirent **d = NULL;
char *cond_dirs[] = {
_PATH_CONDSYS,
_PATH_CONDSYS "/pwr",
};
char path[384];
int i, n;
for (i = 0; i < (int)NELEMS(cond_dirs); i++) {
if (mkpath(cond_dirs[i], 0755) && errno != EEXIST) {
warn("Failed creating %s condition directory, %s", COND_SYS,
cond_dirs[i]);
return;
}
}
n = scandir(_PATH_SYSFS_PWR, &d, NULL, alphasort);
for (i = 0; i < n; i++) {
char *nm = d[i]->d_name;
char buf[10];
snprintf(path, sizeof(path), "%s/%s/type", _PATH_SYSFS_PWR, nm);
if (!fgetline(path, buf, sizeof(buf)) && is_ac(buf)) {
num_ac++;
snprintf(path, sizeof(path), "%s/%s/online", _PATH_SYSFS_PWR, nm);
if (!fgetline(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)
sys_cond("pwr/ac", 1);
}
static void set_logging(int prio)
{
setlogmask(LOG_UPTO(prio));
level = prio;
}
static void toggle_debug(int signo)
{
(void)signo;
debug ^= 1;
set_logging(debug ? LOG_DEBUG : LOG_NOTICE);
}
static void shut_down(int signo)
{
(void)signo;
running = 0;
}
/*
* Started by Finit as soon as possible when base filesystem is up,
* modules have been probed, or insmodded from /etc/finit.conf, so by
* now we should have /sys/class/power_supply/ available for probing.
* If none is found we assert /sys/pwr/ac condition anyway, this is what
* systemd does (ConditionACPower) and also makes most sense.
*/
int main(int argc, char *argv[])
{
struct sockaddr_nl nls = { 0 };
struct pollfd pfd;
char buf[1024];
if (argc > 1) {
if (!strcmp(argv[1], "-d"))
debug = 1;
}
if (!debug) {
openlog("keventd", LOG_PID, LOG_DAEMON);
set_logging(LOG_NOTICE);
logon = 1;
}
signal(SIGUSR1, toggle_debug);
signal(SIGTERM, shut_down);
init();
pfd.events = POLLIN;
pfd.fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
if (pfd.fd == -1)
panic("failed creating netlink socket");
nls.nl_family = AF_NETLINK;
nls.nl_pid = 0;
nls.nl_groups = -1;
if (bind(pfd.fd, (void *)&nls, sizeof(struct sockaddr_nl)))
panic("bind failed");
logit(LOG_DEBUG, "Waiting for events ...");
while (running) {
char *path = NULL;
int ac = 0;
int i, len;
if (-1 == poll(&pfd, 1, -1)) {
if (errno == EINTR)
continue;
break;
}
len = recv(pfd.fd, buf, sizeof(buf), MSG_DONTWAIT);
if (len == -1) {
switch (errno) {
case EINTR:
continue;
case ENOBUFS:
warn("lost events");
continue;
default:
panic("unhandled");
continue;
}
}
buf[len] = 0;
logit(LOG_DEBUG, "%s", buf);
/* skip libusb events, focus on kernel events/changes */
if (strncmp(buf, "change@", 7))
continue;
/* XXX: currently limited to monitoring this subsystem */
if (!strstr(buf, "power_supply"))
continue;
i = 0;
while (i < len) {
char *line = buf + i;
logit(LOG_DEBUG, "%s", line);
if (!strncmp(line, "DEVPATH=", 8)) {
path = &line[8];
logit(LOG_DEBUG, "Got path %s", path);
} else 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)
sys_cond("pwr/ac", 1);
num_ac_online++;
} else {
if (num_ac_online > 0)
num_ac_online--;
if (!num_ac_online)
sys_cond("pwr/ac", 0);
}
}
i += strlen(line) + 1;
}
}
close(pfd.fd);
return 0;
}
/**
* Local Variables:
* indent-tabs-mode: t
* c-file-style: "linux"
* End:
*/
+1
View File
@@ -54,6 +54,7 @@ void dbus_unregister_service (svc_t *svc);
void dbus_notify_service_state (svc_t *svc, int old_state, int new_state);
void dbus_notify_condition_change(const char *name, const char *state);
void dbus_notify_runlevel_change(int old_level, int new_level);
void dbus_notify_reload (void);
#endif
void conf_flush_events(void);
+13 -1
View File
@@ -193,7 +193,19 @@ int run_parts(char *dir, char *cmd, const char *env[], int progress, int sysv)
#ifndef __FINIT__
static int usage(int rc)
{
warnx("usage: runparts [-bdhps?] DIRECTORY");
fprintf(stderr,
"Usage: runparts [OPTIONS] DIRECTORY\n"
"\n"
"Run all executable scripts in DIRECTORY in alphabetical order.\n"
"Scripts prefixed S<NUM> are called with 'start', K<NUM> with 'stop'.\n"
"\n"
"Options:\n"
" -b Batch mode, disable progress output and ANSI colors\n"
" -d Enable debug output\n"
" -h This help text\n"
" -p Show progress and exit status for each script\n"
" -s SysV compat, only run S<NUM>name and K<NUM>name scripts\n"
"\n");
return rc;
}
+71
View File
@@ -1442,6 +1442,77 @@ int service_stop(svc_t *svc)
return rc;
}
/**
* service_stop_now - Stop a service on user request
* @svc: Service to stop
*
* Shared by the legacy API and the D-Bus methods, like
* service_reload().
*
* Returns:
* POSIX OK(0) on success, non-zero if @svc is NULL.
*/
int service_stop_now(svc_t *svc)
{
if (!svc)
return 1;
service_timeout_cancel(svc);
svc_stop(svc);
service_step(svc);
if (!IS_RESERVED_RUNLEVEL(runlevel))
service_step_all(SVC_TYPE_ANY);
return 0;
}
/**
* service_start_now - Start a service on user request
* @svc: Service to start
*
* Returns:
* POSIX OK(0) on success, non-zero if @svc is NULL.
*/
int service_start_now(svc_t *svc)
{
if (!svc)
return 1;
service_timeout_cancel(svc);
svc_start(svc);
service_step(svc);
if (!IS_RESERVED_RUNLEVEL(runlevel))
service_step_all(SVC_TYPE_ANY);
return 0;
}
/**
* service_restart_now - Restart a service on user request
* @svc: Service to restart
*
* Does not wait for @svc to stop first, that is the caller's
* (initctl) responsibility. Otherwise we'd block PID 1, or
* introduce some nasty race conditions.
*
* Returns:
* POSIX OK(0) on success, non-zero if @svc is NULL.
*/
int service_restart_now(svc_t *svc)
{
if (!svc)
return 1;
if (!svc_is_running(svc))
return service_start_now(svc);
service_timeout_cancel(svc);
service_stop(svc);
service_step(svc);
return 0;
}
/**
* service_reload - Request reload of a service, driven by the state machine
* @svc: Service to reload
+3
View File
@@ -60,6 +60,9 @@ void service_forked (svc_t *svc);
void service_ready (svc_t *svc, int ready);
int service_stop (svc_t *svc);
int service_start_now (svc_t *svc);
int service_stop_now (svc_t *svc);
int service_restart_now (svc_t *svc);
int service_reload (svc_t *svc);
int service_step (svc_t *svc);
void service_step_all (int types);
+50
View File
@@ -94,6 +94,53 @@ static void shutdown_wdt_cb(uev_t *w, void *arg, int events)
do_shutdown(halt);
}
/*
* Emergency shutdown bypass, same job as the shutdown watchdog above
* but armed by the user: `initctl -t SEC reboot`, on either transport.
*/
static void bypass_shutdown(void *unused)
{
(void)unused;
cprintf("TIMEOUT TIMEOUT SHUTTING DOWN NOW!!\n");
do_shutdown(halt);
}
static struct wq emergency = { .cb = bypass_shutdown };
void shutdown_bypass(int timeout)
{
if (timeout <= 0)
return;
emergency.delay = timeout * 1000;
schedule_work(&emergency);
}
/*
* User-requested runlevel change, shared by the legacy API and the
* D-Bus SetRunlevel method. Refused in runlevel 0/6, deferred via
* cfglevel during bootstrap.
*/
void sm_request_runlevel(int lvl)
{
if (IS_RESERVED_RUNLEVEL(runlevel) && runlevel != INIT_LEVEL) {
warnx("Cannot abort runlevel 6/0.");
return;
}
if (lvl == 0)
halt = SHUT_OFF;
if (lvl == 6)
halt = SHUT_REBOOT;
/* Runlevel S: user requested change in next runlevel */
if (runlevel == INIT_LEVEL)
cfglevel = lvl;
else
sm_runlevel(lvl);
}
/*
* Console input callback - handles Ctrl-C during bootstrap wait
*/
@@ -586,6 +633,9 @@ restart:
service_step_all(SVC_TYPE_ANY);
dbg("Reconfiguration done");
#ifdef HAVE_DBUS
dbus_notify_reload();
#endif
sm.state = SM_RUNNING_STATE;
break;
}
+2
View File
@@ -35,6 +35,8 @@ void sm_step (void);
int sm_in_reload (void);
void sm_reload (void);
void sm_runlevel (int newlevel);
void sm_request_runlevel(int lvl);
void shutdown_bypass(int timeout);
#endif /* FINIT_SM_H_ */
+8
View File
@@ -49,6 +49,7 @@ EXTRA_DIST += failing-sysv.sh
EXTRA_DIST += svc-env.sh
EXTRA_DIST += global-envs.sh
EXTRA_DIST += initctl-status-subset.sh
EXTRA_DIST += keventd.sh
EXTRA_DIST += notify.sh
EXTRA_DIST += pidfile.sh
EXTRA_DIST += stale-pidfile.sh
@@ -82,6 +83,7 @@ EXTRA_DIST += dbus-service.sh
EXTRA_DIST += dbus-cond.sh
EXTRA_DIST += dbus-initctl.sh
EXTRA_DIST += dbus-introspect.sh
EXTRA_DIST += dbus-device.sh
AM_TESTS_ENVIRONMENT = SYSROOT='$(abs_builddir)/sysroot/';
AM_TESTS_ENVIRONMENT += export SYSROOT;
@@ -112,6 +114,9 @@ TESTS += failing-sysv.sh
TESTS += svc-env.sh
TESTS += global-envs.sh
TESTS += initctl-status-subset.sh
if KEVENTD
TESTS += keventd.sh
endif
TESTS += notify.sh
TESTS += pidfile.sh
TESTS += stale-pidfile.sh
@@ -146,6 +151,9 @@ TESTS += dbus-service.sh
TESTS += dbus-cond.sh
TESTS += dbus-initctl.sh
TESTS += dbus-introspect.sh
if KEVENTD
TESTS += dbus-device.sh
endif
TESTS += fuzz-msg-parse.sh
# Needs the plugin to bring up the bus, not just the built-in one
if BUILD_DBUS_PLUGIN
+140
View File
@@ -0,0 +1,140 @@
#!/bin/sh
# keventd: org.finit.Device1 on /run/keventd/bus.
#
# Covers the device manager's own bus: introspection, queue-state
# properties, Settle, Info by devpath, RulesReload, Trigger, and the
# DeviceProcessed signal. Network interfaces are the one device class
# the unprivileged sandbox can hotplug, see keventd.sh.
set -eu
TEST_DIR=$(dirname "$0")
test_teardown()
{
say "Running test teardown."
run "ip link del dummy0 2>/dev/null || true"
run "rm -f $FINIT_CONF"
}
# shellcheck source=/dev/null
. "$TEST_DIR/lib/setup.sh"
# shellcheck source=/dev/null
. "$TEST_DIR/lib/dbus-setup.sh"
DEVBUS=/run/keventd/bus
if ! texec initctl -p status keventd >/dev/null 2>&1; then
skip "keventd not enabled in this build"
fi
if ! run "ip link add probe0 type dummy 2>/dev/null"; then
skip "cannot create dummy interfaces in test namespace"
fi
run "ip link del probe0"
say "keventd readiness"
retry 'assert_ready "keventd"' 50 0.2
retry "texec test -S $DEVBUS"
say "Device1 introspects with methods, properties, and signal"
xml=$(texec "$CLIENT" introspect "$DEVBUS" /org/finit/device)
for m in Settle Trigger Info RulesReload; do
case "$xml" in
*"<method name=\"$m\">"*) assert "$m declared" 0 -eq 0 ;;
*) fail "$m missing from Device1 XML" ;;
esac
done
case "$xml" in
*'<property name="QueueEmpty" type="b"'*'<signal name="DeviceProcessed">'*)
assert "QueueEmpty property and DeviceProcessed signal declared" 0 -eq 0 ;;
*) fail "property/signal declarations missing: $xml" ;;
esac
say "Queue-state properties are readable"
seq1=$(texec "$CLIENT" getprop "$DEVBUS" /org/finit/device \
org.finit.Device1 SeqnumProcessed)
assert "SeqnumProcessed is numeric (got: $seq1)" "$seq1" -ge 0
qe=$(texec "$CLIENT" getprop "$DEVBUS" /org/finit/device \
org.finit.Device1 QueueEmpty)
case "$qe" in
true|false) assert "QueueEmpty reads $qe" 0 -eq 0 ;;
*) fail "Unexpected QueueEmpty value: $qe" ;;
esac
say "Device1.Settle answers within its timeout"
texec "$CLIENT" call-u "$DEVBUS" /org/finit/device \
org.finit.Device1 Settle 15 >/dev/null \
|| fail "Settle returned non-zero"
assert "Settle completed" 0 -eq 0
say "Interface add advances SeqnumProcessed and Info answers"
run "ip link add dummy0 type dummy"
retry 'assert_cond "class/net/dummy0"'
seq2=$(texec "$CLIENT" getprop "$DEVBUS" /org/finit/device \
org.finit.Device1 SeqnumProcessed)
assert "SeqnumProcessed advanced ($seq1 -> $seq2)" "$seq2" -gt "$seq1"
info=$(texec "$CLIENT" call-s "$DEVBUS" /org/finit/device \
org.finit.Device1 Info /devices/virtual/net/dummy0 2>&1) \
|| fail "Info failed: $info"
assert "Info answered for dummy0" 0 -eq 0
say "keventd -S settles via the bus"
run "/libexec/finit/keventd -S -t 15" || fail "keventd -S failed"
assert "bus-first settle ok" 0 -eq 0
say "Trigger(add, net) re-emits events, DeviceProcessed observed"
rm -f /tmp/dbus-dev-sig.out
( texec "$CLIENT" monitor-signal "$DEVBUS" \
"type='signal',interface='org.finit.Device1',member='DeviceProcessed'" \
5000 > /tmp/dbus-dev-sig.out 2>&1 ) &
mon_pid=$!
sleep 0.5
texec "$CLIENT" call-ss "$DEVBUS" /org/finit/device \
org.finit.Device1 Trigger add net >/dev/null 2>&1 \
|| fail "Trigger returned non-zero"
set +e
wait "$mon_pid"
mon_rc=$?
set -e
assert "monitor saw DeviceProcessed (rc=$mon_rc)" "$mon_rc" -eq 0
case "$(cat /tmp/dbus-dev-sig.out)" in
*"DeviceProcessed"*net*) assert "Signal payload names a net device" 0 -eq 0 ;;
*) fail "Unexpected signal output: $(cat /tmp/dbus-dev-sig.out)" ;;
esac
say "RulesReload succeeds and Trigger rejects a bogus action"
texec "$CLIENT" call-void "$DEVBUS" /org/finit/device \
org.finit.Device1 RulesReload >/dev/null \
|| fail "RulesReload failed"
assert "RulesReload ok" 0 -eq 0
set +e
texec "$CLIENT" call-ss "$DEVBUS" /org/finit/device \
org.finit.Device1 Trigger frobnicate "" >/tmp/dbus-trg.out 2>&1
trg_rc=$?
set -e
assert "Bogus action rejected (rc=$trg_rc)" "$trg_rc" -eq 1
case "$(cat /tmp/dbus-trg.out)" in
*InvalidArgs*) assert "Error is InvalidArgs" 0 -eq 0 ;;
*) fail "Unexpected reply: $(cat /tmp/dbus-trg.out)" ;;
esac
say "Device1.RulesReload from non-root is rejected with AccessDenied"
texec chmod 0666 "$DEVBUS"
set +e
texec "$CLIENT" call-void-as-uid 1 "$DEVBUS" /org/finit/device \
org.finit.Device1 RulesReload >/tmp/dbus-devauthz.out 2>&1
dauthz_rc=$?
set -e
assert "Non-root RulesReload rejected (rc=$dauthz_rc)" "$dauthz_rc" -eq 1
case "$(cat /tmp/dbus-devauthz.out)" in
*AccessDenied*) assert "Device1 authz fires" 0 -eq 0 ;;
*) fail "Unexpected reply: $(cat /tmp/dbus-devauthz.out)" ;;
esac
say "Conditions are generation symlinks, unaffected by initctl reload"
run "initctl reload"
retry 'assert_cond "class/net/dummy0"'
assert "class/net/dummy0 still asserted after reload" 0 -eq 0
+7
View File
@@ -41,6 +41,13 @@ texec initctl reload >/dev/null \
|| fail "initctl reload returned non-zero"
assert "initctl reload ok" 0 -eq 0
# keventd was restarted above and only writes its pidfile once the
# coldplug queue has drained, so <pid/keventd> asserts a good while
# after the restart returns. Let it land before subscribing below,
# or the monitor catches keventd's ConditionChanged instead of ours
# and stops, since it waits for exactly one signal.
retry 'assert_cond pid/keventd' 100 0.1
# A ConditionChanged signal can only originate from Cond1.Set going
# through finit (the legacy filesystem path doesn't emit signals).
# So if the monitor sees one, we know initctl cond set was routed
+33
View File
@@ -29,3 +29,36 @@ case "$xml" in
fail "per-character <arg> fragment leaked" ;;
*) assert "no bracket/array fragments" 0 -eq 0 ;;
esac
# ---------- <signal> declarations ----------
say "Cond1 declares the ConditionChanged signal"
case "$xml" in
*'<signal name="ConditionChanged">'*) assert "ConditionChanged declared" 0 -eq 0 ;;
*) fail "ConditionChanged missing from Cond1 XML" ;;
esac
say "Manager1 declares ServiceStateChanged and RunlevelChanged"
mgr=$(texec "$CLIENT" introspect "$BUS" /org/finit/manager)
case "$mgr" in
*'<signal name="ServiceStateChanged">'*'<signal name="RunlevelChanged">'*)
assert "Manager1 signals declared" 0 -eq 0 ;;
*) fail "Manager1 signal declarations missing" ;;
esac
say "Properties declares PropertiesChanged, org.freedesktop.DBus is present"
case "$mgr" in
*'<signal name="PropertiesChanged">'*) assert "PropertiesChanged declared" 0 -eq 0 ;;
*) fail "PropertiesChanged missing from Properties XML" ;;
esac
# Hello/AddMatch/RemoveMatch are answered on the canonical object
# only, so they must be declared there and nowhere else.
drv=$(texec "$CLIENT" introspect "$BUS" /org/freedesktop/DBus)
case "$drv" in
*'<interface name="org.freedesktop.DBus">'*) assert "org.freedesktop.DBus declared on canonical object" 0 -eq 0 ;;
*) fail "org.freedesktop.DBus missing from /org/freedesktop/DBus" ;;
esac
case "$mgr" in
*'<interface name="org.freedesktop.DBus">'*) fail "org.freedesktop.DBus leaked onto /org/finit/manager" ;;
*) assert "org.freedesktop.DBus not advertised off-path" 0 -eq 0 ;;
esac
+46
View File
@@ -137,3 +137,49 @@ case "$rl" in
*)
fail "Unexpected initctl runlevel output: $rl" ;;
esac
# ---------- legacy-parity edge semantics ----------
say "Manager1.SetRunlevel with bogus level returns InvalidArgs"
set +e
texec "$CLIENT" call-u "$BUS" /org/finit/manager \
org.finit.Manager1 SetRunlevel 42 >/tmp/dbus-rl.out 2>&1
rl_rc=$?
set -e
assert "Bogus runlevel rejected (rc=$rl_rc)" "$rl_rc" -eq 1
case "$(cat /tmp/dbus-rl.out)" in
*InvalidArgs*) assert "Error is InvalidArgs" 0 -eq 0 ;;
*) fail "Unexpected reply: $(cat /tmp/dbus-rl.out)" ;;
esac
say "Manager1.Signal on a stopped service returns Failed"
cat >> "$SYSROOT$FINIT_CONF" <<CONF
service stopped-serv {
manual = true
command = "serv -np"
}
CONF
run "initctl reload"
set +e
texec "$CLIENT" call-su "$BUS" /org/finit/manager \
org.finit.Manager1 Signal stopped-serv 1 >/tmp/dbus-sig.out 2>&1
sig_rc=$?
set -e
assert "Signal to stopped service rejected (rc=$sig_rc)" "$sig_rc" -eq 1
case "$(cat /tmp/dbus-sig.out)" in
*Failed*) assert "Error is Failed" 0 -eq 0 ;;
*) fail "Unexpected reply: $(cat /tmp/dbus-sig.out)" ;;
esac
# Reboot cannot be invoked without shutting down the sandbox, so
# verify the timeout argument via introspection instead.
say "Reboot family declares the timeout argument in introspection"
flat=$(printf '%s' "$xml" | tr -d '\n')
for m in Reboot Poweroff Halt; do
case "$flat" in
*"<method name=\"$m\"> <arg type=\"u\" direction=\"in\"/>"*)
assert "$m takes (u)" 0 -eq 0 ;;
*)
fail "$m does not declare the timeout argument" ;;
esac
done
+22
View File
@@ -94,3 +94,25 @@ case "$(cat /tmp/dbus-sig.out)" in
*)
fail "Unexpected signal output: $(cat /tmp/dbus-sig.out)" ;;
esac
# ---------- name:id identities and _HH path encoding ----------
say "Manager1.GetService(dhcp-client:eth1) hex-escapes the object path"
cat >> "$SYSROOT$FINIT_CONF" <<CONF
service dhcp-client:eth1 {
manual = true
command = "serv -np"
}
CONF
run "initctl reload"
epath=$(texec "$CLIENT" get-service "$BUS" "dhcp-client:eth1")
assert "Encoded path (got: $epath)" "$epath" = "/org/finit/service/dhcp_2dclient_3aeth1"
say "The encoded service object introspects and answers Properties.Get"
exml=$(texec "$CLIENT" introspect "$BUS" "$epath")
case "$exml" in
*'org.finit.Service1'*) assert "Service1 on encoded path" 0 -eq 0 ;;
*) fail "Service1 missing on $epath" ;;
esac
eident=$(texec "$CLIENT" getprop "$BUS" "$epath" org.finit.Service1 Identity)
assert "Identity round-trips (got: $eident)" "$eident" = "dhcp-client:eth1"
+73
View File
@@ -0,0 +1,73 @@
#!/bin/sh
#
# Verify keventd, the bundled device manager. Network interfaces are
# the only devices an unprivileged test can hotplug: the sandbox has
# its own network namespace, so `ip link add` makes the kernel emit
# genuine uevents for keventd to process.
#
# Verifies that keventd signals readiness, that an interface add event
# asserts <class/net/IFNAME> to start a dependent service -- but never
# <dev/IFNAME>, interfaces are not device nodes -- that the
# /run/udev/data entry uses libudev-style n<ifindex> keying, and that
# the remove event reverses it all.
#
set -eu
TEST_DIR=$(dirname "$0")
test_teardown()
{
say "Running test teardown."
run "ip link del dummy0 2>/dev/null || true"
run "rm -f $FINIT_CONF"
}
# shellcheck source=/dev/null
. "$TEST_DIR/lib/setup.sh"
if ! texec initctl -p status keventd >/dev/null 2>&1; then
skip "keventd not enabled in this build"
fi
if ! run "ip link add probe0 type dummy 2>/dev/null"; then
skip "cannot create dummy interfaces in test namespace"
fi
run "ip link del probe0"
sep "keventd readiness"
retry 'assert_ready "keventd"' 50 0.2
sep "interface add asserts <class/net/dummy0>"
cat >> "$SYSROOT$FINIT_CONF" <<EOF
service serv {
log { file = "/dev/null" }
conditions = { "class/net/dummy0" }
command = "serv -np"
}
EOF
run "cat $FINIT_CONF"
run "initctl reload"
assert_status "serv" "waiting"
run "ip link add dummy0 type dummy"
retry 'assert_cond "class/net/dummy0"'
assert_nocond "dev/dummy0"
retry 'assert_status "serv" "running"'
sep "udev database uses n<ifindex> keying"
ifindex=$(texec cat /sys/class/net/dummy0/ifindex)
assert_file_exists "/run/udev/data/n$ifindex"
assert_file_contains "/run/udev/data/n$ifindex" "E:INTERFACE=dummy0"
sep "conditions survive initctl reload"
run "initctl reload"
retry 'assert_cond "class/net/dummy0"'
retry 'assert_status "serv" "running"'
sep "interface remove clears <class/net/dummy0>"
run "ip link del dummy0"
retry 'assert_nocond "class/net/dummy0"'
retry 'assert_status "serv" "waiting"'
retry "texec test ! -e /run/udev/data/n$ifindex"
+10 -8
View File
@@ -43,15 +43,17 @@ _libs_nss := $(firstword $(wildcard /lib/$(ARCH)-linux-gnu/libnss_files.so.
# one test can check Finit against dbus-daemon instead of only against
# libink's own client. Absent is fine, dbus-broker.sh skips.
dbus_bins := $(foreach b,dbus-daemon dbus-send,$(firstword $(wildcard /usr/bin/$(b) /bin/$(b))))
# Given several binaries ldd prefixes each with a 'path:' header, and
# that trailing colon would land in a make target. Excluding it here
# is enough, no need for one ldd per binary.
_libs_dbus := $(if $(dbus_bins),$(shell ldd $(dbus_bins) | grep -Eo '/[^ :]+'))
# The binaries stage exactly like the libraries: same host path, same
# path under DEST, copied by the rule below.
_libs_src := $(shell ldd $(FINITBIN) | grep -Eo '/[^ ]+') $(_libs_nss) \
$(_libs_dbus) $(dbus_bins)
# Bundled helpers like keventd link more libraries than finit itself,
# and so does dbus-daemon. One ldd per binary: given several at once
# ldd prefixes each with a 'path:' header, and that trailing colon
# would land in a make target.
_bins := $(FINITBIN) $(dbus_bins) \
$(shell find $(abspath $(DEST)) -path '*libexec/finit*' -type f -perm -u+x 2>/dev/null)
# The dbus binaries stage exactly like the libraries: same host path,
# same path under DEST, copied by the rule below.
_libs_src := $(foreach bin,$(_bins),$(shell ldd $(bin) 2>/dev/null | grep -Eo '/[^ ]+')) \
$(_libs_nss) $(dbus_bins)
libs := $(foreach path,$(sort $(_libs_src)),$(abspath $(DEST))$(path))
all: $(libs) $(DEST)/bin/$(BBBIN)
+7
View File
@@ -30,6 +30,13 @@ fi
timeout=50
while [ "$pid" -le 1 ]; do
sleep 0.1
# Once Finit gives up on a service it forks the post:script and
# reports that PID as the service's, so a slay still waiting for
# the service to come back would kill the script instead.
st=$(initctl -p status "$nm" 2>/dev/null | awk '/Status/{print $3}')
if [ "$st" = "crashed" ]; then
exit 1
fi
pid=$(getpid "$nm")
if [ "$pid" -le 1 ]; then
timeout=$((timeout - 1))
+71 -1
View File
@@ -322,6 +322,58 @@ static int mode_call_s(int argc, char *argv[])
return do_call(argv[2], argv[3], argv[4], argv[5], argv[6]);
}
/* "u" and "su" variants for methods taking a uint32 argument */
static int do_call_u(const char *sock, const char *obj, const char *iface,
const char *method, const char *arg, uint32_t val)
{
link_client_t *c;
int rc;
c = link_client_open(sock);
if (!c) return 2;
rc = arg
? link_client_call_v(c, obj, iface, method, "su", arg, val)
: link_client_call_v(c, obj, iface, method, "u", val);
rc = report_rc(c, rc);
if (rc == 0)
printf("OK\n");
link_client_close(c);
return rc;
}
static int mode_call_ss(int argc, char *argv[])
{
link_client_t *c;
int rc;
if (argc != 8) return 2;
c = link_client_open(argv[2]);
if (!c) return 2;
rc = link_client_call_v(c, argv[3], argv[4], argv[5],
"ss", argv[6], argv[7]);
rc = report_rc(c, rc);
if (rc == 0)
printf("OK\n");
link_client_close(c);
return rc;
}
static int mode_call_u(int argc, char *argv[])
{
if (argc != 7) return 2;
return do_call_u(argv[2], argv[3], argv[4], argv[5], NULL,
(uint32_t)strtoul(argv[6], NULL, 0));
}
static int mode_call_su(int argc, char *argv[])
{
if (argc != 8) return 2;
return do_call_u(argv[2], argv[3], argv[4], argv[5], argv[6],
(uint32_t)strtoul(argv[7], NULL, 0));
}
static int mode_call_void(int argc, char *argv[])
{
if (argc != 6) return 2;
@@ -375,7 +427,7 @@ static int mode_get_service(int argc, char *argv[])
/*
* getprop BUS PATH IFACE PROP -- Properties.Get, prints the variant
* value; dispatches on the wire signature, "s" and "u" supported.
* value; dispatches on the wire signature: s, u, b, and t.
*/
static int mode_getprop(int argc, char *argv[])
{
@@ -405,6 +457,21 @@ static int mode_getprop(int argc, char *argv[])
printf("%s\n", s);
else if (type == 'u' && link_r_u32(&reader, &u) == 0)
printf("%u\n", u);
else if (type == 'b') {
int b;
if (link_r_bool(&reader, &b) == 0)
printf("%s\n", b ? "true" : "false");
else
rc = 2;
} else if (type == 't') {
uint64_t t;
if (link_r_u64(&reader, &t) == 0)
printf("%llu\n", (unsigned long long)t);
else
rc = 2;
}
else {
fprintf(stderr, "unsupported variant type '%c'\n", type);
rc = 2;
@@ -513,6 +580,9 @@ int main(int argc, char *argv[])
if (!strcmp(argv[1], "introspect")) return mode_introspect (argc, argv);
if (!strcmp(argv[1], "liststrings")) return mode_liststrings (argc, argv);
if (!strcmp(argv[1], "call-s")) return mode_call_s (argc, argv);
if (!strcmp(argv[1], "call-ss")) return mode_call_ss (argc, argv);
if (!strcmp(argv[1], "call-u")) return mode_call_u (argc, argv);
if (!strcmp(argv[1], "call-su")) return mode_call_su (argc, argv);
if (!strcmp(argv[1], "call-void")) return mode_call_void (argc, argv);
if (!strcmp(argv[1], "call-s-as-uid")) return mode_call_s_as_uid (argc, argv);
if (!strcmp(argv[1], "call-void-as-uid")) return mode_call_void_as_uid(argc, argv);