How to Test vdev
================

This document is meant to help early adopters help me test vdev, and make sure it works with a wide variety of hardware.  The reader is expected to be familiar with basic shell commands and have basic familiarity with Makefiles.

Step 1: Compile and Install vdev
--------------------------------

The steps to compile and prepare vdevd for testing are as follows:

    $ git clone https://github.com/jcnelson/vdev vdev
    $ cd vdev
    $ make build
    # Regenerate hwdb.squashfs from *.hwdb sources (slow): make all

As a result of these commands, you will have checked out the latest copy of vdev's source code, and built and installed the hotplug daemon `vdevd`.

Step 2: Set up a Fake /dev
--------------------------

`vdevd` is capable of running on a live system with `udevd` or `mdev` or the like, but we will need to ensure it creates device nodes somewhere else besides `/dev`.  To do so, we'll set up a `tmpfs` mount:

    $ mkdir vdev-test
    $ sudo mount -t tmpfs none vdev-test/

`vdevd` will populate the directory `vdev-test/` with device nodes in the next step.

There is a way to detach the busy device immediately - even if it is busy and cannot be unmounted forcefully. You may cleanup all later:

	$ sudo umount -l vdev-test

Step 3: Generate a Fake /dev
----------------------------

`vdevd` has the option of scanning the contents of `/sys` and exiting once it finishes.  We call this "run once" mode.  It's useful for generating a static `/dev`, or updating a previously-generated static `/dev` with only the device files that correspond to hardware plugged into your computer.  We will use this feature to test `vdevd`.

From the root of the source code repository you checked out in step 1, the command to have `vdevd` generate a fake `/dev` is:

    $ sudo vdevd/vdevd -v2 -c example/vdevd.conf -l /tmp/vdev.log --once vdev-test/

`vdevd` should print out some runtime setup information to your console, run for a few seconds, and exit normally.  If you run `ls vdev-test/`, you should see a bunch of device files.  All debugging information will have been written to `/tmp/vdev.log`.

Step 4: Inspect the Fake /dev and Logfile
-----------------------------------------

If you'd like, feel free to explore the `vdev-test/` directory and skim through the `/tmp/vdev.log` logfile, and note any missing or extra device files and symlinks.

You might notice the directory `vdev-test/vdev/`.  This directory contains device attributes for each device processed (specifically, the contents of the `uevent` packet `vdevd` processed, as well as a few other things documented in the Appendix).  Programs that need to monitor the system for devices and query device properties need only look in this directory.

Step 5: Send Me a Listing of /dev and the Logfile
-------------------------------------------------

Once you're satisfied, please run `tree /dev` and `tree vdev-test/`, and send me the resulting directory tree listings so I can figure out what device files and symlinks `vdevd` failed to create.  Also, please send me the logfile `/tmp/vdev.log`.

If either the listings or the `/tmp/vdev.log` file contains information you'd rather not share, please take the time to trim that information out (and please note it in the listings).  In particular `/tmp/vdev.log` will contain a full listing of all the hardware plugged into your computer, including device serial numbers and network MAC addresses.  I will keep the information to myself and not share with anyone, but nevertheless the information will be sent over the public Internet.

Once you're satisfied, please email them to `judecn@devuan.org`.

Thank you very much for reading!

Appendix A: A Crash-Course to vdevd
-----------------------------------

`vdevd` has a `--help` switch that prints out the directives it understands.  In particular, you can:

* make it run in the foreground with `-f`.  This is great if you want to watch it process hardware events (but this requires `-v1` or `-v2`).
* change the log verbosity with `-v $LEVEL`.  Substitute `$LEVEL` with 0 for logging only drastic errors; 1 for logging non-critical notices, and 2 for logging debug messages.
* change the log file with `-l $PATH_TO_LOGFILE`.  If you omit this, `vdevd` logs to stdout.
* write a PID file with `-p $PATH_TO_PIDFILE`.  This is useful only when running `vdevd` as a daemon.

`vdevd` works internally by bufferring up device events from the kernel, matching device events against "actions," and running an action's associated script if it matches.  The vdev project comes with a set of actions that are meant to make `vdevd` behave as close as possible to udev.  You can find them in `example/actions/`, and you can find the Linux-specific scripts and binaries the actions execute in `vdevd/helpers/LINUX/`.

Actions are defined as ini-files.  Unless stated otherwise, all action fields are optional.  All fields must match the device event for the action's command to be executed.  The fields are:

* `event`: **Required**.  Use "add" to match a device-add event, "remove" to match a device-remove event, "change" to match a device-change event (such as a battery level changing), or "any" to match any device event.
* `path`: This is an extended POSIX regular expression that matches the kernel-given device path (such as `sda` or `input/mice`) to the action.
* `type`: This is either "block" or "char", which will match the event only if it it is for a block or character device, respectively.  Not all devices are representable by block or character device files, however, which is why this field is optional.
* `OS_*`: Any field prefixed with `OS_` matches an OS-specific device attribute.  On Linux, these include names of `uevent` fields.  For example, an action can match a device's subsystem using the `OS_SUBSYSTEM` field.  Non-empty values are exact matches, or `|`-separated alternatives (`OS_SUBSYSTEM=pci|block|usb`).  If an empty value is given (e.g. "`OS_SUBSYSTEM=`"), then the action will match any value as long as the device attribute is present in the device event (for example, a Linux `uevent` packet that does not have `SUBSYSTEM` defined will not match an action with `OS_SUBSYSTEM=`).

Once a device event is encountered that matches all of an action's fields, `vdevd` runs the commands specified by the action's ini file.  The fields that describe the commands are:

* `rename_command`:  This is a shell command to run to generate the path to the device file to create.  It will be evaluated after the device event matches the action, but before the device file is created.  The command must write the desired path to stdout.  Its output will be truncated at 4,096 characters (which is `PATH_MAX` on most filesystems).  Note that not all devices have paths given by the kernel.
* `command`:  This is the shell command to run once the device file has been successfully created.  Unless specified otherwise, `vdevd` will run this as a subprocess--it will wait until the command has completed before processing the next action.
* `async`:  If set to "True", this tells `vdevd` to continue processing the action immediately after running its `command`.  This is useful for long-running device setup tasks, where it is undesirable to block `vdevd`.
* `daemonlet`:  If set to "True", this tells `vdevd` to run the command as a *daemonlet*.  See "Advanced Device Handling" below for a description of what this means.

**Caveat**:  `vdevd` matches a device event against actions according to their files' lexographic order.  Moreover, it processes device events sequentially, in the order in which they arrive.  This is also true for the Linux port--the kernel's SEQNUM field is ignored at this time (but is passed on to actions).

`vdevd` communicates device information to action commands using environment variables.  When a `command` or `rename_command` runs, the following environment variables will be set:

* `$VDEV_ACTION`:  This is the `event` field of the action (i.e. "add", "remove", "change", or "any").
* `$VDEV_DAEMONLET`: If set to 1, then the command is being invoked as a daemonlet (see the "Advanced Device Handling" subsection below).
* `$VDEV_CONFIG_FILE`: This is the absolute path to the configuration file `vdevd` is using.
* `$VDEV_HELPERS`:  This is the absolute path to the directory containing `vdevd`'s helper programs.
* `$VDEV_INSTANCE`:  This is a randomly-generated string that corresponds to this running intance of `vdevd`.  It will be different on each invocation of `vdevd`.
* `$VDEV_GLOBAL_METADATA`:  The absolute path to the metadata directory into which `vdevd` writes metadata (e.g. `/dev/metadata`).
* `$VDEV_LOGFILE`:  If specified, this is the path to `vdevd`'s logfile.
* `$VDEV_MAJOR`:  If the device event corresponds to a block or character device, this is the device file's major number.
* `$VDEV_METADATA`:  If the device is given a path (either by the kernel or the `rename_command` output), this is the absolute path to the directory to contain the device's metadata.
* `$VDEV_MINOR`:  If the device event corresponds to a block or character device, this is the device file's minor number.
* `$VDEV_MODE`:  If the device event corresponds to a block or character device, this is the string "block" or "char" (respectively).
* `$VDEV_MOUNTPOINT`:  This is the absolute path to the directory in which the device files will be created.  For example, this is usually `/dev`.
* `$VDEV_OS_*`:  Any OS-specific device attributes will be encoded by name, prefixed with `VDEV_OS_`.  For example, on Linux, the SUBSYSTEM field in the device's `uevent` packet will be exported as `$VDEV_OS_SUBSYSTEM`.  Similarly, the DEVPATH field in a device's `uevent` packet will be exported as `$VDEV_OS_DEVPATH`.
* `$VDEV_PATH`:  If the device is given a path (either by the kernel or the `rename_command` output), this is the path to the device file *relative* to `$VDEV_MOUNTPOINT`.  The absolute path to the device file can be found by `$VDEV_MOUNTPOINT/$VDEV_PATH`.  Note that not all devices have device files (such as batteries, PCI buses, etc.).


**Advanced Device Handling (daemonlets)**

By default, `vdevd` `fork()`s and `exec()`s the action helper once per matching
event. Setting `daemonlet=true` starts a long-lived **runner** instead, so
`vdevd` does not pay that process startup on every common event (`modprobe`,
`udev-compat`, etc.).

The runner is **`/lib/vdev/daemonlet`**, a small **C** program
(`vdevd/helpers/LINUX/daemonlet.c`). It used to be a **dash** wrapper that
applied each request with `eval "$_ENV_NAME=\"$_ENV_VALUE\""`. That is unsafe:
the values come from kernel uevents / sysfs (device names, `DEVPATH`,
modalias, …), the process runs as **root**, and a quote, backtick, or `$()` in
a value could execute shell. The C runner uses **`setenv(3)`** after checking
that the key is a valid identifier (`[A-Za-z_][A-Za-z0-9_]*`). Values are never
passed through `eval`.

What stays a shell script is the **helper** (`modprobe.sh`, `udev-compat.sh`,
…). It defines `main()` and skips the one-shot call at the bottom when
`$VDEV_DAEMONLET=1`. The runner, for each stanza, `fork()`s a short-lived
`dash -c '. "$1"; main; exit $?'` with the installed environment. Helper
stdout goes to `/dev/null`; the runner reports the exit status to `vdevd`.

Protocol between `vdevd` and the runner (unchanged):

1. `vdevd` `exec()`s `/lib/vdev/daemonlet <helper-script>` (not a shell).
2. The runner writes a readiness newline to stdout.
3. For each event, `vdevd` writes newline-terminated `NAME=VALUE` strings,
   then `done`. An empty line / EOF asks the runner to exit.
4. The runner writes an ASCII exit code (plus newline) for that request.
   `0` is success.

If the runner or helper dies, `vdevd` logs it and tries to restart the
daemonlet.


Appendix B: Booting with vdevd
-------------------------------

**NOTE: These instructions are Debian- and Devuan-specific, and very hacky.  Use at your own risk.**

**WARNING: Readers are expected to know how to fix a broken initramfs and a broken bootsystem if they try this.**

Running `make build && sudo make install` will get you most of the way towards installing vdev (use `make all` only if you need to rebuild the hwdb database).  But to use it, you will need to disable udev, enable vdev, and rebuild your initramfs to include vdev instead of udev.

On Debian and most Debian-derived distributions, it is possible to generate an initramfs image with this command:

    $ cd example/ && make initramfs

This will generate an initramfs image in `example/`, which can be installed with your bootloader of choice.

### Initramfs: `bind` / `unbind` vs real-root settle

The initrd profile (`profiles.initrd` → **`vdevd.initrd.conf`**, **`actions.initrd`**) only
cares about **pci / block / usb** storage for **`ROOT`** and **`crypttab`**. Init-top
seeds with **`vdevadm trigger -s pci|block|usb -a add`**. Modprobe stays limited to
those subsystems via **`002-modalias.act`** (`OS_SUBSYSTEM=pci|block|usb`).
Init-bottom stops with **`kill -TERM "$(vdevadm control --print-pid)"`** and moves
**`/dev`** onto the real root.

**`vdevd` bind/unbind support is useful in initrd passively:** the early listener
keeps running after seeding (netlink + control; no `-H` / `hotplug_only`). If late USB
(or a driver that attaches after **`add`** was processed) emits a kernel **`bind`**
uevent, actions with **`event=add`** still match and storage helpers run — no initrd
change required.

**Do not mirror real-root settle in initrd:**

* No **`vdev-settle-rebind-input.sh`** or HID driver unbind/bind (no input in
  **`actions.initrd`**; rebind could drop a block device mid-mount).
* No **`trigger --action=bind`** in the pci → block → usb loop (**`add`** is enough for
  cold seed).
* No libinput / **`vdev_settle`** phase (display manager is not up yet).

For the full contrast table and boot diagram context, see **`example/README.md`**
(*Initramfs (`initrd`) and `bind` / `unbind`*).

**Edge-case manual test** (initramfs shell, not automated policy): device in sysfs, no
**`/dev`** node after **`add`** → **`vdevadm trigger -a bind /sys/...`**.

To enable vdev and disable udev in the init system, the command is 

    $ cd example/ && make install-initscript

I'm still working on the packaging scripts that will do all of this automatically.


Misc
----

Thanks for reading!  Please send any questions, comments, and bug reports to the Devuan mailing list (`dng@lists.dyne.org`)

Appendix C: vdevd.globals.json (profiles and subsystem lists)
---------------------------------------------------------------

`vdevd` supports an optional JSON file (`vdevd.globals.json`) next to the active
`config_path` (typically `/etc/vdev/`). The **daemon** loads **`sysfs`**,
**`config-dir`**, **`profiles.long-running.config`**, and optional
**`profiles.*.actions`** allowlists. Init scripts and **`vdevadm`** still read
**`profiles.*.subsystems`** / **`stamp`** via **`jq`**.

**Boot narrative (1.5.5+):** **`/etc/init.d/vdev`** starts **`vdevd.early`**
(**`actions.early`**, control bound). **`vdev-coldplug`** (oneshot, background)
waits for **`/run/udev/control`**, triggers early subsystems, runs
**`vdevadm settle`**, promotes with **`vdevadm control --promote`**, touches
**`vdev.ready`**, then **`vdevadm trigger --sysfs`**. On **`vdev` restart** when
**`vdev.ready`** exists, start long-running **`vdevd.conf`** directly. See
**`example/README.md`**.

**Comments:** **`vdev_globals_json_strip_comments()`** removes **`//`** and
**`/* */`** outside JSON strings; see **`example/README.md`**.

Example (`/etc/vdev/vdevd.globals.json`) (abbreviated — see **`example/`** /
**`debian/config/`** for the full layout):

    {
      "globals": {
        "sysfs": "/sys",
        "config-dir": "/etc/vdev",
        "states-stamp-dir": "/run/vdev"
      },
      "profiles": {
        "initrd": {
          "config": "vdevd.initrd.conf",
          "subsystems": ["pci", "block", "usb"],
          "actions": ["001-sysfs-block.act", "001-sysfs-char.act",
                      "002-modalias.act", "char.act", "device-mapper.act"]
        },
        "early": {
          "config": "vdevd.early.conf",
          "subsystems": ["graphics", "video4linux", "input", "hid", "i2c", "drm", "fd"],
          "stamp": "vdev.ready",
          "actions": ["002-modalias.act", "dri.act", "fb.act", "input.act",
                      "seat.act", "uaccess.act", "v4l.act", "zzz-udev-compat.act"]
        },
        "terminal": {
          "config": "vdevd.1.conf",
          "stamp": "vdevd-1-coldplug.ready",
          "actions": "*"
        },
        "long-running": { "config": "vdevd.conf", "actions": "*" }
      }
    }

Field meaning:

* `globals.sysfs`: sysfs mount root.
* `profiles.*`: profile basenames — **`early`**, **`long-running`**, optional
  **`terminal`** / **`initrd`**, etc. (no **`modprobe`** phase in the current boot).
* `profiles.*.subsystems`: ordered lists for **`vdevadm trigger -s`** (scripts), not
  walked by **`vdevd`** C code.
* `profiles.*.actions`: which **`*.act`** basenames from the profile’s INI
  **`actions=`** directory to load. Use **`"*"`** (or **`["*"]`**) for all files
  in that directory. Files present on disk but omitted here are skipped (so
  discarded early/initrd rules need not be removed in **`postinst`**). If the
  field is absent, every file in the directory is loaded (compat).
* **`[vdev-config] queue_tracking_flag`** in each **`vdevd.*.conf`**: synthetic
  **`QUEUE_TRACKING_FLAG`** on the last coldplug-only sysfs uevent. JSON **`stamp`**
  fields are for scripts / **`os-queue-tracking.sh`**.
  **`vdev.ready`** is touched by **`vdev-coldplug` after promote**, not by
  **`os-queue-tracking.sh`**.

**Semantics — do not confuse with `coldplug_only`:** **`queue_tracking_flag`**
(when **`> 0`**) is an **index**, not shorthand for **`coldplug_only`**. Reserve
**`coldplug_only=1`** for short-lived oneshots (e.g. **`vdevd.1.conf`** with
**`queue_tracking_flag=2`**). Early / long-running are hotplug listeners (netlink +
control). Promote uses **`vdevadm control --promote`** (same PID). Discover the
listener PID with **`vdevadm control --print-pid`** (requires the control socket).
Foreground **`coldplug_only`** tests: **Ctrl+C** (**`SIGINT`**) stops **`vdevd`**;
there is no control socket on that instance.
Mechanically **`vdevd`** tags the **last** coldplug-queue uevent so
**`zzzz-os-queue-tracking.act`** can **`touch`** a **`.ready`** file and
**`SIGTERM`** **`$PPID`** for coldplug-only instances. Behaviour lives in
**`vdevd/helpers/LINUX/os-queue-tracking.sh`**.

In other words **`queue_tracking_flag` > 0** lines up with **one-shot
booking / sequencing hooks** (**"coldplug queue for this phase is done -> do X"**),
which is orthogonal to **`coldplug_only`** (**"avoid netlink/listener backends for
this process"**) — combining both is coherent for the terminal **`vdevd.1`** pattern.

Quick verification:

1) Edit one subsystem list in `vdevd.globals.json` (e.g. remove `hidraw` from
   `profiles.early.subsystems`).
2) Run `vdevd` in foreground with debug (`-v2`) on your test mountpoint.
3) Confirm in logs that processing order/coverage follows the edited list.
4) Restore defaults if required.

Settle-mode quick check:

1) Enable in JSON:
   `globals["settle-no-libinput"] = true`.
2) Run:
   `vdev_settle /dev/metadata/udev/input --quiet --no-libinput`
3) Verify it exits 0 when target input names are present in
   `/proc/bus/input/devices`, or times out (non-zero) otherwise.

**Note — `os-queue-tracking.sh` is not a substitute for settle:** that helper only
reacts to **`QUEUE_TRACKING_FLAG`** and touches **`.ready`** stamps for phase
boundaries; it does not classify uevents against **`/proc/bus/input/devices`**, and
kernel registration vs userspace (**`libinput`** / **`seat0`**) can diverge in time and
scope. Treat **`globals["settle-no-libinput"]`** / **`vdev_settle --no-libinput`** as an
optional **kernel-side** readiness probe, not as logic that belongs inside
**`os-queue-tracking.sh`**. See **`example/README.md`** (“Why `os-queue-tracking.sh`
cannot replace settle classification”).

Seat / seatd / DRM udev database check
--------------------------------------

After boot with vdev (or once **`/run/vdev/vdev.ready`** exists), run the
regression helper installed as **`/lib/vdev/check-seat-udev.sh`**:

    sudo /lib/vdev/check-seat-udev.sh

It waits for the ready stamp (default **`/run/vdev/vdev.ready`**,
timeout **`VDEV_SEAT_CHECK_TIMEOUT`** seconds, default 60), then verifies:

* **`/run/udev`** points at the udev-compat tree;
* each **`/dev/dri/card*`** has group **`video`**, mode **`0660`**, and udev records
  **`G:seat`**, **`G:master-of-seat`**, **`E:ID_SEAT=seat0`** under
  **`/dev/metadata/udev/data/cMAJ:MIN`** (and **`tags/seat/`** index entries);
* each **`/dev/dri/renderD*`** has seat metadata and render (or video) group;
* **`/dev/fb*`** nodes, if present, have seat tags (and **`master-of-seat`** on **`fb0`**);
* the seat tag index is non-empty;
* **sound** **`uaccess`** udev records (if any) include **`E:DEVNAME=/dev/snd/...`**
  (browser ACL sync — see **ALSA and desktop browser audio** below).

Exit codes: **0** pass, **1** failure, **2** stamp timeout. Override paths with
**`VDEV_PHASE1_STAMP`** (default **`vdev.ready`**), **`VDEV_UDEV_ROOT`**, or
**`VDEV_MOUNTPOINT`** when testing a non-default **`/dev`** layout.

Long-running actions are active after **`vdevadm control --promote`** (same PID as
early); optional terminal coldplug (**`vdevd.1`** `-1`) is **not** required for seat
checks. After boot, **`getfacl /dev/tty1`** on the active VT
should show a **`user:<you>:rw-`** ACL from elogind if the session stack is enabled.
**`/etc/init.d/vdev stop`** should exit 0 (no OpenRC error).

ALSA and desktop browser audio
------------------------------

This section documents end-to-end verification for **ALSA CLI playback**, **elogind
session ACLs** on **`/dev/snd/*`**, and **Abrowser / Firefox** ( **`pulse-rust`**
backend) **without** a PulseAudio/PipeWire user daemon and **without** **apulse** —
matching typical **eudev + elogind** behaviour on a laptop with **Intel HDA analog**
speakers and a **GPU HDMI** audio device.

### Hardware layout (example)

Many laptops expose two **`snd_hda_intel`** codecs:

| Card | Typical role        | `/proc/asound/cardN/id` |
|------|---------------------|-------------------------|
| 0    | NVIDIA HDMI         | `NVidia` or `HDMI`      |
| 1    | Intel PCH analog    | `PCH`                   |

Without explicit ordering, **`aplay -D default`** may open **HDMI (silent on laptop
speakers)** while **`plughw:1,0`** works. Fix with **modprobe index** + **`asound.conf`**
(below).

### `/etc/modprobe.d/alsa.conf` (card index)

Assign stable **ALSA card numbers** so analog PCH is not card 0 when HDMI appears first.
Example matching a dual-**`snd_hda_intel`** machine:

```bash
# /etc/modprobe.d/alsa.conf
options snd-hda-intel id=NVidia index=0
options snd-hda-intel id=PCH index=1
```

Notes:

* Module parameter **`id=`** matches the codec **PCI id string** (see **`aplay -l`** /
  **`/proc/asound/cardN/id`**). Names are case-sensitive (**`NVidia`**, **`PCH`**).
* Some distributions document the module as **`snd_hda_intel`** (underscore); use the
  name your **`modprobe.d`** and **`/lib/modprobe.d/`** snippets expect.
* Rebuild the initramfs or reboot after changing this file so early boot and **`alsactl`**
  see a consistent card numbering.

Verify after boot:

```bash
aplay -l
cat /proc/asound/card0/id
cat /proc/asound/card1/id
```

### `/etc/asound.conf` (default PCM and control)

Route **`default`** to the **analog** codec by **card id** (survives reorder better than
numeric index alone). Example for **PCH** playback device 0:

```bash
# /etc/asound.conf
pcm.!default {
    type plug
    slave.pcm "hw:PCH,0"
}
ctl.!default {
    type hw
    card PCH
}
```

The **`type plug`** PCM allows rate/format conversion; **`hw:PCH,0`** is the analog
playback device. The shipped package installs the same layout from
**`debian/vdev-asound-analog.conf`** → **`/etc/asound.conf`**.

**User overrides:** **`~/.asoundrc`** or **`~/.config/alsa/asoundrc`** load **after**
**`/etc/asound.conf`** and can break **`default`** (for example a bare **`type hw`**
without **`plug`**). Remove or align user files if **`aplay -D default`** diverges from
system config.

Verify:

```bash
aplay -D default -v /usr/share/sounds/alsa/Front_Center.wav
# expect: Hardware PCM card 1 'HDA Intel PCH' device 0  (index may differ; id must be PCH)
amixer -c PCH sget Master | grep -E 'Playback|\[on\]|\[off\]'
```

**OGG test file:** **`aplay`** does not decode OGG; use **`ffplay`** for
**`~/UNI-JSON/audio_test.ogg`** or similar.

### vdev action chain (udev rule mapping)

Reference udev rules live under **`~/UNI-JSON/udev-rules/`** (or **`/lib/udev/rules.d/`**
on the target system). Installed vdev actions (**`actions.early`** for early hotplug /
**`vdevd.early.conf`**, **`actions/`** for long-running **`vdevd.conf`**) include:

| vdev action | udev analogue | Helper |
|-------------|---------------|--------|
| **`sound-alsa-control.act`** | **`60-persistent-alsa.rules`** | **`sound.sh`** — **`snd/by-path`**, **`snd/by-id`**, group **`audio`**, mode **`0660`** |
| **`70-sound-uaccess.act`** | **`70-uaccess.rules`** + **`73-seat-late.rules`** | **`uaccess.sh`** — tag **`uaccess`**, **`elogind-uaccess-command`**, **`audio-acl.sh`** |
| **`78-sound-card-control.act`** | **`78-sound-card.rules`** (control) | **`sound-card.sh`** — **`change`** on **`controlC*`** → card uevent |
| **`78-sound-card.act`** | **`78-sound-card.rules`** (card) | **`sound-card.sh`** — **`SOUND_*` / `ID_*`** properties |
| **`90-alsa-restore.act`** | **`90-alsa-restore.rules`** | **`alsa-restore.sh`** — **`alsactl restore`**, post-restore unmute on analog cards |
| **`seat.act`** ( **`snd/card[0-9]+`** ) | **`71-seat.rules`** (partial) | **`seat.sh`** — tag **`seat`**, group **`audio`** |
| **`zzz-udev-compat.act`** | (n/a — libudev DB) | **`udev-compat.sh`** — **`E:DEVNAME`**, tags, events |

**`sound-audio.act`** (legacy **`/dev/audio`**) remains in **`example/actions/`** only;
modern stacks use **`snd/*`** nodes.

Lexicographic order matters: **`70-sound-uaccess`** runs before **`zzz-udev-compat`**, so
tags and **`VDEV_SEAT`** exist before the udev-compat DB is written.

### Why browser audio needs udev **`DEVNAME`**

**Abrowser** (Firefox **145**, **`pulse-rust`**) runs audio in a **sandbox** that does
not inherit the **`audio`** group. **eudev** applies session ACLs via **`73-seat-late.rules`**:

```text
TAG=="uaccess", RUN+=/usr/libexec/elogind-uaccess-command %N $env{ID_SEAT}
```

**vdev** has no udev **`RUN`** hook; **`uaccess.sh`** calls the same binary and
**`audio-acl.sh`** (**`setfacl`**) on **`snd/*`**. **elogind** also re-syncs ACLs at login
from **`/run/udev/data/cMAJ:MIN`**. That record **must** include at least:

```text
E:DEVNAME=/dev/snd/pcmC1D0p
E:SUBSYSTEM=sound
E:ID_SEAT=seat0
G:uaccess
```

If only **`G:uaccess`** is present (no **`E:DEVNAME`**), CLI tools like **`aplay`** may
work (group **`audio`**) while **Abrowser stays silent**. **`udev-compat.sh`** now emits
the core **`E:`** fields from the device environment.

**`/run/user/$UID/pulse/`** is created by **libpulse** when the browser starts; it does
**not** imply a PulseAudio daemon. **Do not** rely on **`media.cubeb.backend=alsa`** on
this build — only **`pulse-rust`** / **`pulse`** backends are compiled in.

### Deploy and refresh after package or helper updates

```bash
sudo cp vdevd/helpers/LINUX/{udev-compat,uaccess,audio-acl,alsa-restore,sound,sound-card,dev-setup}.sh /lib/vdev/
sudo cp example/actions/{sound-alsa-control,70-sound-uaccess,78-sound-card-control,78-sound-card,90-alsa-restore}.act /etc/vdev/actions/
sudo cp debian/config/actions.early/{sound-alsa-control,70-sound-uaccess,78-sound-card-control,78-sound-card,90-alsa-restore}.act /etc/vdev/actions.early/

sudo systemctl restart vdev   # or /etc/init.d/vdev restart

for ctl in /sys/class/sound/controlC*; do
  echo change | sudo tee "$ctl/uevent" >/dev/null
done
```

### Verification checklist

1. **Nodes and permissions**

   ```bash
   ls -la /dev/snd/
   getfacl /dev/snd/pcmC1D0p /dev/snd/controlC1
   ```

   Expect **`user:<you>:rw-`** on playback/control nodes after login.

2. **udev-compat DB**

   ```bash
   grep -E '^E:(DEVNAME|SUBSYSTEM|ID_SEAT)|^G:uaccess' /run/udev/data/c116:11
   sudo /lib/vdev/check-seat-udev.sh
   ```

3. **ALSA helpers (optional deep test)**

   ```bash
   ./example/test-alsa-actions.sh --dry-run
   ./example/test-browser-audio.sh ~/UNI-JSON/audio_test.ogg
   ```

4. **Browser**

   Close all Abrowser windows, open from the menu (not **`apulse`**), play video with
   sound. **`MOZ_LOG=cubeb:5 abrowser`** can show backend init if debugging.

### Common failures

| Symptom | Likely cause |
|---------|----------------|
| **`aplay -D default`** silent, **`plughw:PCH,0`** works | Missing or wrong **`/etc/asound.conf`** / user **`asoundrc`** override |
| **`Master [off]`** | Stale **`/var/lib/alsa/asound.state`**; run **`90-alsa-restore`** or **`alsactl restore`** |
| CLI OK, browser silent, ACL missing | **`70-sound-uaccess`** / **`uaccess.sh`** not deployed |
| CLI OK, browser silent, ACL present, no **`E:DEVNAME`** | Old **`udev-compat.sh`** — upgrade and retrigger **`snd`** uevents |
| **`chown: /dev/snd/seq`** in logs | Fixed in **`dev-setup.sh`** (static **`snd/seq`**, **`snd/timer`**) |

See also **`example/actions/README.md`** (ALSA actions) and **`example/test-alsa-actions.sh`**.

Input hotplug: Xlibre vs X.Org (not a vdev defect)
--------------------------------------------------

After boot, **vdev**, **seatd**, and **udev-compat** can all behave correctly while
**hotplugged** evdev nodes still fail to move the pointer in **Xlibre** started from a
console session (for example **`/etc/X11/Xsession startxfce4`**). This is **not** a vdev
metadata or **`event-put`** bug. The same stack with **X.Org** (`xorg` / **`/usr/lib/xorg/Xorg`**)
does **not** show the problem in the same setup.

**Observed with Xlibre (regression checklist):**

* Coldplug at server start works (devices present before X starts, or after restarting X
  with the device already plugged in).
* Disconnect/reconnect or a newly plugged **`input/event*`** node may not respond until
  X is restarted, unless a workaround below is used.
* **`vdevadm info`**, **`/dev/metadata/udev/data/cMAJ:MIN`**, and
  **`/run/vdev/vdev-settle.seen`** show the device with **`G:seat`**, **`G:uaccess`**, and
  **`ID_SEAT=seat0`** — vdev did its job.
* **`evtest /dev/input/eventN`** as the session user can read events (direct node access
  is fine).
* **`xinput list`** does **not** list the hotplugged device until the workaround applies.
* **`~/.local/share/xorg/Xorg.0.log`** (or the active X log) shows, on hotplug:
  **`(EE) libinput: … Failed to create a device for /dev/input/eventN`**.

**Not observed with X.Org:** hotplugged pointer/keyboard nodes continue to work without
restarting X and without adding the session user to the **`input`** group.

**Typical Xlibre session environment (console login):**

* **`XDG_SEAT=seat0`**, **`XDG_VTNR`**, and **`DBUS_SESSION_BUS_ADDRESS`** reach the X
  process — good.
* **`XDG_SESSION_TYPE=tty`** often remains even when hotplug works — do **not** treat
  **`export XDG_SESSION_TYPE=x11`** alone as a fix (it may not appear on the Xorg process
  and is not required if you use the startup path below).

**Root cause (Xlibre stack, not vdev):**

The failure is tied to **whether the desktop is started via `startx` (through `~/.xinitrc`) or by
running the same command directly on the login TTY**, not to **`/etc/X11/Xsession`** itself
(users report **`exec /etc/X11/Xsession startxfce4`** in **`~/.xinitrc`** behaves like
**`exec startxfce4`** when both are launched with **`startx`**).

| How you launch | Hotplug (typical) |
|----------------|-------------------|
| **`startx`** ( **`~/.xinitrc`** runs **`exec startxfce4`**, **`exec openbox-session`**, **`exec /etc/X11/Xsession …`**, etc.) | Works |
| **Same command typed on the TTY** (e.g. **`startxfce4`**, **`openbox-session`**, without **`startx`**) | Broken |

**Why `startx` matters (not the text inside `.xinitrc`):**

**`startx`** is a wrapper around **`xinit`**: it sets up **auth** ( **`xauth`** ), often **VT**
arguments, **`unset SESSION_MANAGER`**, then starts the X server and runs **`~/.xinitrc`** as
the **client** only after the server is coming up.

**`startxfce4`** from a TTY (no **`startx`**) calls **`xinit`** itself with
**`/etc/xdg/xfce4/xinitrc`** as the client — it does **not** read **`~/.xinitrc`**. The X
server and the session stack start in the **single `xinit` invocation** XFCE uses by default.

So **`startx` + `exec startxfce4`** is **not** the same as **`startxfce4`** on the TTY:

1. **`startx`** → **`xinit`** starts **Xorg**, then **`~/.xinitrc`** → **`startxfce4`** sees
   **`DISPLAY`** already set and runs **`/etc/xdg/xfce4/xinitrc`** under **`sh`** (X already up).
2. **`startxfce4`** on the TTY → **`xinit /etc/xdg/xfce4/xinitrc`** in one step (different
   timing and parent chain for **Xorg** vs **xfce4-session**).

That difference (and whatever **`startx`** adds: **`.xserverrc`**, **cookies**, VT) likely
explains why **libinput** hotplug works in one case and logs
**`(EE) libinput: … Failed to create a device for /dev/input/eventN`** in the other.
**`XDG_SESSION_TYPE`** may stay **`tty`** in both; exporting **`x11`** is not the reliable fix.

**X.Org** does not show this split in the same setup. **`setfacl`**, **`seatd`**, and
**`/etc/X11/Xsession`** alone do not explain the difference.

At **X server restart**, libinput **re-enumerates** and succeeds — hence “restarting X fixes it”.

**Workarounds (Xlibre + console session):**

1. **Preferred:** always start the desktop via **`startx`** and a minimal **`~/.xinitrc`**:

        #!/bin/sh
        exec startxfce4

   (or **`exec openbox-session`**, **`exec /etc/X11/Xsession startxfce4`**, etc. — what matters
   is **`startx`**, not which **`exec`** line you use.)

2. Add the session user to the **`input`** group and re-login:
   **`sudo usermod -aG input $USER`**
3. Use a display manager (for example **lightdm**).
4. Restart X after connecting new input hardware (re-enumeration only).

**Compare the two setups (replace PIDs after each test):**

    # After startx + .xinitrc
    ps -o ppid=,cmd= -p "$(pgrep -n Xorg)"
    tr '\0' '\n' < /proc/$(pgrep -n Xorg)/environ | sort | grep -E '^(DISPLAY|XAUTHORITY|XDG_|DBUS)'

    # After startxfce4 from TTY (broken hotplug case)
    # run the same three commands and diff

**Quick diagnostics after hotplug (replace `eventN`):**

    # vdev / libinput monitor daemon (separate libinput context — not X itself)
    cat /run/vdev/vdev-settle.seen | grep eventN

    # udev-compat static database
    ID=$(stat -c '%t:%T' /dev/input/eventN | tr '[:lower:]' '[:upper:]')
    grep -E '^(G:|E:ID_SEAT)' /dev/metadata/udev/data/c"$ID"

    # direct node access vs X adoption
    evtest /dev/input/eventN
    xinput list | grep -i mouse
    grep -i 'Failed to create a device' ~/.local/share/xorg/Xorg.0.log

    # confirm Xlibre libinput stack (expect libinput + libudev; no seatd/logind)
    ldd /usr/lib/xorg/modules/input/libinput_drv.so | grep -iE 'libinput|udev|seat|logind|systemd'

**Misleading checks:**

* **`vdev_settle list`** is a FIFO command, not a CLI subcommand — use
  **`cat /run/vdev/vdev-settle.seen`** or the FIFO documented in **`vdev_settle --help`**.
* A line in **`vdev-settle.seen`** only proves **vdev_settle’s** libinput context saw the
  device; it does **not** prove Xlibre adopted it.
