Devuan packaging of vdev - a device-file manager and filesystem
===============================================================

NOTE: this project is a fork of [Jude Nelson's vdev
project](https://git.devuan.org/unsystemd/vdev), which at this point
is an orphaned project. This project is aimed at the preparation of
Devuan packaging of the components.

The resulting packages are:

> `libudev1-compat`, which is the library ABI-compatible with
  libudev 219.

> `vdevd`, which is the hotplug daemon, including helpers, actions and
  configuration;

> `vdev-initramfs`, which provides an initramfs-tools intergration.
  This package includes a script (/usr/share/vdev/vdev-install.sh) for
  making a manual choice between using vdev or udev as /dev manager.

Go to
[release](https://git.devuan.org/ralph.ronnquist/vdev/tree/master/release)
if all you want is to download the package files I have built. Pick
the latest version of each, then install with

    # dpkg -i *.deb

That will also make a `vdev` choice. You can then, whenever, alternate
between `udev` and `vdev` via (in a terminal):

    # /usr/share/vdev/vdev-install.sh dialog

Note: `Vdev` competes with `udev` on three fronts: 1) the library link
`libudev-so.1`, 2) the scripts for the initrd, and 3) the sysvinit
`/etc/rcS.d` scripts. The current solution deals with these aspects
underneath and invisibly for the Devuan (Debian) package management,
rather than straight out replacing `udev` and `libudev1`.

Therefore it's probably **safest to always shift to `udev` before
installing** other things. I don't think it's vital, but there are
many possible failure cases, and that is why I give that advice.

Other Notes
-----------

Please refer to [Jude Nelson's vdev
project](https://git.devuan.org/unsystemd/vdev) for an extensive
description of the software.

Packaging of the userspace filesystem, vdevfs, is forthcoming.

---

## Architecture: vdev, eventfs, and libudev-compat

This section documents how device events reach programs that link against
**libudev-compat** (the `libudev1-compat` package), and how that differs from
**vdevfs** and stock **libudev**.

### Layers at a glance

| Component | Role |
|-----------|------|
| **vdevd** | Hotplug daemon: sysfs/netlink, actions, helpers |
| **dev-setup.sh** | Preseed on every `vdevd` start: `/dev` layout, udev metadata tree, **eventfs mount** |
| **eventfs** (`/bin/eventfs`) | Separate FUSE daemon; RAM-backed FIFO queues under `serial/` |
| **event-put** | Helper: writes uevents into `global/` then multicasts to each `serial/libudev-*/` queue |
| **udev-compat.sh** | Action: builds udev DB + calls `event-put` for libudev-compat consumers |
| **libudev-compat** | Replaces netlink with per-process queue dirs on the eventfs mount |
| **vdevfs** (optional) | Different FUSE layer: ACL/policy on `/dev` access — not event transport |

### vdevd/os profile model

The Linux backend no longer encodes `modprobe` / `early` / `initrd` as C
profiles. It only distinguishes a hotplug **listener** (`!coldplug_only`) from a
**oneshot** sysfs walk (`coldplug_only`). Subsystem seed lists stay in
[`debian/config/vdevd.globals.json`](debian/config/vdevd.globals.json) for
scripts / `vdevadm` (`jq`). The only profile basename the daemon loads from
JSON is `profiles.long-running.config` → `globals.long_running` (promote target).

```mermaid
flowchart LR
  subgraph listener ["!coldplug_only"]
    NL[netlink]
    CTRL["/run/udev/control"]
    PROMOTE["vdevadm control --promote"]
    RELOAD["vdevadm control --reload"]
    PROMOTE --> Promote[promote to long_running]
    RELOAD --> Reload[actions_reload]
  end
  subgraph once ["coldplug_only"]
    SYSFS[sysfs walk all devices]
    FLAG[QUEUE_TRACKING on last]
  end
  scripts["init scripts / vdevadm"] -->|subsystems from JSON| Trigger[vdevadm trigger -s]
  Trigger --> NL
```

- **Promote / reload** only via the control socket (`VDEV_CTRL_PROMOTE` /
  `VDEV_CTRL_RELOAD`). A stray **`SIGHUP`** is ignored; **Ctrl+C** (`SIGINT`)
  exits in foreground / `coldplug_only` test runs.
- Listeners bind **`/run/udev/control`**; `coldplug_only` does not.
- See **`example/README.md`** for the boot services (`vdev` + `vdev-coldplug`).

The **eventfs** Debian package ships only `/bin/eventfs` and `/etc/eventfs/`.
Mount lifecycle is owned by **vdev** (`dev-setup.sh` mount, `vdev-eventfs-umount.sh`
on shutdown). There is no separate `eventfs` init service.

### Event path layout

```
/dev/metadata/udev/events/
├── global/              # staging for event-put (brief)
└── serial/              # eventfs FUSE mount
    └── libudev-<pid>-<slot>/   # one queue per libudev-compat monitor
        ├── head -> <seqnum>
        ├── tail -> <seqnum>
        └── <seqnum>            # uevent file
```

`/run/udev` → symlink to `/dev/metadata/udev` (created by preseed).

### Boot and mount lifecycle

Boot is two services (**`vdev`** + **`vdev-coldplug`**); see **`example/README.md`**
for the promote / seed sequence. Eventfs mount still happens inside **`vdevd`**
preseed (no separate `eventfs` init unit):

1. SysV/OpenRC starts **`vdev`**: **`vdevd.early`** (listener + control), or
   **`vdevd.conf`** if **`/run/vdev/vdev.ready`** already exists. No
   `Required-Start: eventfs`.
2. **`vdev-coldplug`** (background): wait for control → seed early subsystems →
   settle → **`vdevadm control --promote`** → touch **`vdev.ready`** →
   **`vdevadm trigger --sysfs`**.
3. On every **`vdevd` start**, **preseed** runs **`dev-setup.sh`**, which calls
   `eventfs_mount()`:
   - Creates `metadata/udev/*` directories and `/run/udev`.
   - If the serial queue is already a **live** FUSE mount (`stat -f '%T'` is
     `fuse`, `fuseblk`, or `fuse.*`, and `ls` succeeds): **exit without remounting**.
   - Otherwise starts `setsid /bin/eventfs -o nonempty,allow_other -c /etc/eventfs/eventfs.conf … &`
     and waits until healthy.
4. After promote, long-running actions (**udev-compat.sh**, …) feed **event-put**.

```mermaid
flowchart TB
    subgraph boot [Boot]
        vinit[vdev init]
        cold[vdev-coldplug bg]
        vdevd[vdevd early then promote]
        preseed[dev-setup.sh preseed]
        vinit --> vdevd
        cold -->|"control --promote"| vdevd
        vdevd --> preseed
        preseed -->|"eventfs_mount if not healthy"| spawn[setsid /bin/eventfs]
    end

    subgraph eventfs_proc [eventfs FUSE daemon - separate OS process]
        mount["/dev/metadata/udev/events/serial"]
        spawn --> mount
    end

    subgraph vdevd_runtime [vdevd runtime]
        actions[udev-compat.sh]
        eventput[event-put]
        actions --> eventput
    end

    subgraph clients [libudev-compat clients]
        monitor["udev_monitor_new_from_netlink(udev)"]
        queue["libudev-PID-slot/"]
        monitor -->|"mkdir + inotify"| queue
    end

    eventput -->|"write + multicast"| mount
    eventput -->|"per-queue copy"| queue
    queue --> mount
```

### Data flow (producer → consumer)

```mermaid
sequenceDiagram
    participant K as kernel / sysfs
    participant V as vdevd
    participant U as udev-compat.sh
    participant E as event-put
    participant F as eventfs FUSE
    participant L as libudev-compat client

    K->>V: uevent
    V->>U: action helper
    U->>E: uevent text on stdin
    E->>F: global/SEQNUM then serial/libudev-*/SEQNUM
    L->>F: mkdir libudev-PID-slot/
    L->>F: inotify on new files
    F-->>L: event via socketpair / epoll
```

### vdev stop, restart, and shutdown

| Event | eventfs behaviour |
|-------|-------------------|
| **`vdevd` preseed** (every start) | Mount only if not already healthy; **never** lazy-unmount on preseed |
| **`service vdev stop`** | Kills `vdevd` only; **eventfs stays mounted** |
| **`service vdev restart`** | Same: preseed sees live mount and skips remount |
| **System shutdown** (runlevels 0/6) | `vdev-eventfs-umount.sh` via SysV `stop` |

After `vdevd` restart, `serial/` may look empty until each client calls
`udev_monitor_new_from_netlink("udev")` again and recreates its queue directory.

```mermaid
flowchart LR
    subgraph running [Normal operation]
        A[vdevd running]
        B[eventfs mounted]
        A --- B
    end

    subgraph restart [vdev restart]
        R1[vdevd_stop]
        R2[vdevd_start + preseed]
        R3{mount healthy?}
        R4[skip remount]
        R1 --> R2 --> R3
        R3 -->|yes| R4
    end

    subgraph halt [Shutdown RL 0/6]
        H1[vdevd_stop]
        H2[vdev-eventfs-umount.sh]
        H1 --> H2
    end
```

### Queue directories: fate sharing and sticky

[eventfs](https://github.com/jcnelson/eventfs) attaches each queue directory under
`serial/` to the process that created it (`mkdir`). By default the directory and
its contents **vanish when that process exits** (fate sharing). If the client sets
the FUSE xattr `user.eventfs_sticky` on the queue directory, it persists until
explicitly removed.

That is a **client-side** concern (libudev-compat right after `mkdir`), not
something `dev-setup` applies to the mountpoint itself:

```sh
setfattr -n user.eventfs_sticky \
  /dev/metadata/udev/events/serial/libudev-$PID-$slot
```

Sticky can let a queue outlive a short-lived creator within the same boot; it does
**not** replace keeping the eventfs FUSE daemon and mount alive across `vdevd`
restart.

### libudev-compat and eventfs detection

**libudev-compat** is eventfs-only for the `"udev"` monitor source: it scans
`/proc/mounts` for `fuse.eventfs` on `/dev/metadata/udev/events/serial`. Without
that mount, `udev_monitor_new_from_netlink()` returns `NULL` / `ENODEV`; use
stock **libudev** for true kernel netlink.

Per monitor it:

1. `mkdir` `/dev/metadata/udev/events/serial/libudev-<pid>-<slot>/`
2. `inotify` watches for new event files
3. Delivers parsed devices via epoll/socketpair (no kernel BPF filter)

**vdevd** producers use **event-put** from **udev-compat.sh**; system **libudev**
(with stock udev) still owns netlink when you use udev as the device manager.

### vdevfs vs eventfs

Do not confuse the two FUSE stacks:

| | **eventfs** | **vdevfs** |
|---|-------------|------------|
| Mount point | `…/metadata/udev/events/serial` | Usually `/dev` |
| Purpose | uevent queues for libudev-compat | Per-caller ACL on device nodes |
| Started by | `dev-setup.sh` preseed | Manual / separate service |
| Replaces the other? | No | No |

See `fs/README.md` for vdevfs ACL examples.

### Key source files

| Path | Purpose |
|------|---------|
| `vdevd/helpers/LINUX/dev-setup.sh` | Preseed + `eventfs_*` mount helpers |
| `vdevd/helpers/LINUX/vdev-eventfs-umount.sh` | Shutdown unmount only |
| `vdevd/helpers/LINUX/event-put.c` | Multicast uevents to queue dirs |
| `vdevd/helpers/LINUX/udev-compat.sh` | udev DB + `event-put` |
| `debian/init` | SysV vdev; shutdown umount hook |
| `/bin/eventfs` (eventfs package) | FUSE implementation |
| `/etc/eventfs/eventfs.conf` | Quotas tuned for many listener dirs |

### Historical note: remount bugs

Kernels may report FUSE type as `fuse` rather than `fuseblk`. Preseed logic that
only checked `fuseblk` treated a healthy mount as absent and attempted a second
mount on top, corrupting the queue. Current `dev-setup.sh` accepts `fuse`,
`fuseblk`, and `fuse.*`, and skips remount when the mount is already alive.
