# Vdev Actions Guide

## Overview

Vdev actions are configuration files that define how the virtual device manager should respond to device events. They specify which commands or helpers to run when devices are added, removed, or changed, with flexible matching criteria and execution options.

## Action File Structure (*.act)

Action files typically have a `.act` extension and are placed in the actions
directory configured by `actions=` in `vdevd*.conf` (commonly
`/etc/vdev/actions`, `/etc/vdev/actions.0`, `/etc/vdev/actions.early`, or
`/etc/vdev/actions.initrd`).

Each file defines one **action**: a rule that may run a **helper** script (under
`/lib/vdev` at runtime, shipped from `vdevd/helpers/LINUX/` in this tree) or an
inline **`command=`** when a device event matches.

The parser uses the same INI style as `vdevd.conf`: section **`[vdev-action]`**,
`key=value` pairs, and **`#`** or **`;`** comments at the beginning of a line
(see `libvdev/ini.c`).

### Basic Action File Format

```ini
[vdev-action]
event=add|remove|change|any
path=regex_pattern
type=block|char
command=shell_command
helper=helper_script_name
async=true|false
daemonlet=true|false
if_exists=error|mask|run
```

## Configuration Fields

### Required Fields

- **`event`**: Device event type that triggers this action
  - `add`: Device creation
  - `remove`: Device removal  
  - `change`: Device modification
  - `any`: Any device event

- **Execution Method** (one of these is required):
  - **`command`**: Shell command to execute (synchronously by default, see below the **`async`** optional field).
  - **`helper`**: script name resolved under `$VDEV_HELPERS` (typically
  `/lib/vdev`). Mutually exclusive with **`command`**.
  - **`rename_command`**: Command to generate new device path

### Optional Fields

- **`path`**: POSIX extended regular expression to match device paths (relative to the devtmpfs mount, e.g. `dri/card0`).
  - Example: `^tty$` matches only "tty"
  - Example: `^sd[a-z][0-9]*$` matches SCSI disks and partitions

- **`type`**: Device type filter; when set, restricts to `block` or `char` (from the
  device request).
  - `block`: Block devices
  - `char`: Character devices

- **`async`**: Run the action asynchronously (default: false). Applies to
  **`command=`** actions and non-daemonlet **`helper=`** actions.
  - `true`: Background execution
  - `false`: Synchronous execution

- **`daemonlet`**: Run as persistent daemon (default: false)
  - `true`: Long-lived **`/lib/vdev/daemonlet`** (C) keeps the pipe to
    **`vdevd`**, applies each event's env with **`setenv(3)`** (no shell
    **`eval`** of values), then runs the helper's **`main`** in a short-lived
    **`dash`**
  - `false`: One-shot helper once per event (see **`vdevd/action.c`**)

- **`if_exists`**: Behavior when device already exists
  - `error`: Fail with error (default)
  - `mask`: Skip action silently
  - `run`: Execute action anyway

  Current tree note: the canonical example is
  **`sound-alsa-control.act`**, which sets **`if_exists=run`** so ALSA/control
  helper logic is still applied even when the node already exists.

- **`queue_tracking_handler`**: Allow execution on negative queue tracking
  - `true`: Run even with negative QUEUE_TRACKING_FLAG
  - `false`: Skip on negative queue tracking (default)

## How actions are chosen

Actions are loaded from the configured **`actions=`** directory. **`vdevd`**
sorts them **lexicographically by filename**, which is the order you normally see
in logs when each action runs: earlier names run first, later names run last.
For example **`udisks2.act`** runs **before** **`zzz-udev-compat.act`**, because
**`u`** sorts before **`z`**. The coldplug / queue-tracking hook is deliberately
named **`zzzz-os-queue-tracking.act`** (four **`z`** characters) so it sorts
**after** **`zzz-udev-compat.act`** and ends up **last** in the list.

## Shell Command Actions vs Helper Scripts

As explained above, commands and helpers are mutually exclusive. The former are
inline commands executed synchronously by default:

```ini
[vdev-action]
event=add
path=^usb[0-9]+
command=/usr/local/bin/setup-usb-device.sh "$VDEV_PATH"
async=true
```

whereas helper scripts are resolved from `$VDEV_HELPERS` (typically `/lib/vdev`)
and receive device information via environment variables.

### Environment Variables Available to Helpers

- **`VDEV_ACTION`**: Event type (add/remove/change)
- **`VDEV_PATH`**: Device path within mountpoint
- **`VDEV_MOUNTPOINT`**: vdev mountpoint directory
- **`VDEV_METADATA`**: Device metadata directory
- **`VDEV_OS_*`**: OS/device request parameters exported from the event (uevent-derived)
- **`VDEV_VAR_*`**: Helper variables exported from `VAR_*` keys in the action
- **`VDEV_GLOBALS_JSON`**: Path to `vdevd.globals.json` (typically
  `/etc/vdev/vdevd.globals.json`); set for helpers from the running `vdevd`
  (not only init scripts). If the process environment omits it, `vdevd`
  defaults to `$globals.config_dir/vdevd.globals.json` from the parsed profile.
  **`vdevd` strips `//` and `/* */` outside strings** before applying its
  light-weight globals parser (**`vdev_globals_json_strip_comments`**). **`jq`** in
  init scripts often uses comparable preprocessing plus validation—see **`example/README.md`**.
- **`VDEV_DAEMONLET`**: 1 if running as daemonlet, 0 otherwise

### Helper Script Structure

```bash
#!/bin/dash
# Source common helper functions
. "$VDEV_HELPERS/subr.sh"

main() {
    # Handle device removal
    if [ "$VDEV_ACTION" = "remove" ]; then
        vdev_cleanup "$VDEV_METADATA"
        return 0
    fi
    
    # Handle device addition
    if [ "$VDEV_ACTION" = "add" ]; then
        # Your logic here
        vdev_symlink "../../$VDEV_PATH" "$VDEV_MOUNTPOINT/custom/link" "$VDEV_METADATA"
        return 0
    fi
    
    vdev_error "Unknown action '$VDEV_ACTION'"
    return 1
}

# Execute based on daemonlet mode
if [ "${VDEV_DAEMONLET:-0}" -eq 0 ]; then 
    main 
    exit $?
fi
```

### Common Helper Functions

From `subr.sh`:
- `vdev_symlink source target metadata`: Create tracked symlink
- `vdev_rmlinks metadata`: Remove all tracked symlinks
- `vdev_cleanup metadata`: Clean up device resources
- `vdev_permissions owner:group mode path`: Set permissions
- `vdev_log message`: Log info message
- `vdev_warn message`: Log warning
- `vdev_error message`: Log error

## The `path=` regex (devtmpfs path vs `OS_DEVPATH`)

The optional **`path=`** line is compiled as a POSIX extended regular expression
(**`REG_EXTENDED`**, **`REG_NEWLINE`**; see **`vdev_match_regex_init()`** /
**`vdev_match_regex()`** in **`libvdev/match.c`**) and matched against the
device’s **node path relative to the devtmpfs mount** — the same string helpers
see as **`VDEV_PATH`** (e.g. **`dri/card0`**, **`sr0`**, **`input/event3`**).
There is **no** leading slash in that relative path for the matcher; patterns are
usually **anchored** with **`^`** and **`$`** so the rule applies only to an exact
name or to a well-defined family of names.

By contrast, **`OS_DEVPATH`** refers to the **`DEVPATH`** field from the kernel
**uevent** / netlink payload: on Linux this is the **sysfs** device path (under
**`/sys/devices/...`**), not the name of the node under **`/dev`**. Use
**`OS_DEVPATH`** (or other **`OS_*`** keys) when you need to match topology
encoded in sysfs; use **`path=`** when you only care about the stable devtmpfs
basename or a regexp over that short string.

### `KERNEL` in udev vs `path` in vdev

Many udev rules use **`KERNEL==...`**. Conceptually, that means “match the
kernel device node name” (e.g. `kbd`, `kvm`, `fb0`, `event3`). In vdev, the
equivalent matcher is **`path=`** against **`VDEV_PATH`**:

- udev **`KERNEL=="kbd"`** ↔ vdev **`path=^kbd$`**
- udev **`KERNEL=="fb[0-9]*"`** (glob) ↔ vdev **`path=^fb[0-9]+$`** (ERE)

Important difference: udev `KERNEL` patterns are shell-style globs, while vdev
`path=` is POSIX ERE.

**ERE reminders:** **`*`** = zero or more, **`+`** = one or more, **`?`** =
optional, **`|`** = alternation inside **`(...)`** grouping.

Examples:

```ini
path=^kvm$
```

Matches only **`kvm`** (the **`/dev/kvm`** character device).

```ini
path=^sr[0-9]*$
```

Optical / SCSI **`sr`**, **`sr0`**, **`sr1`**, … (**`*`** allows zero or more
digits after **`sr`**).

```ini
path=sr.*$
```

Any relative path where the regex can match a substring from **`sr`** to the
end of the string (weaker than an anchored rule; prefer **`^sr[0-9]*$`** when
you mean only names like **`sr0`**, **`sr1`**, …).

```ini
path=^fb[0-9]+$
```

Framebuffer nodes **`fb0`**, **`fb1`**, … (**`+`** requires at least one digit).

```ini
path=^3270/tty[0-9]*$
```

Devices such as **`3270/tty0`** under devtmpfs.

```ini
path=^(dri/(card[0-9]+|renderD[1-9][0-9]*)|input/event[0-9]+)$
```

Same shape as **`seat.act`**: DRM **`dri/card`** plus digits, **or**
**`dri/renderD`** plus digits (with **`renderD`** names whose first digit after
**`renderD`** is **`1`–`9`**, then optional further digits), **or**
**`input/event`** plus digits.

---

## `OS_*` vs `VAR_*` in the `[vdev-action]` section

Both kinds of keys live in the same INI section, but they answer different
questions: OS-specific parameters are prefixed with **`OS_*`** and match against
device properties from sysfs to decide **whether** the action applies to this
device event. Helper-specific variables are prefixed with **`VAR_*`** and passed
as **extra** environment variables to the helper.

### `OS_*` — filter on kernel / synthetic uevent fields

- In the `.act` file you write **`OS_<FIELD>`**, where **`<FIELD>`** is the
  parameter name as carried on the device request (Linux uevent keys are
  usually uppercase: **`SUBSYSTEM`**, **`DEVPATH`**, **`DEVNAME`**, …). The
  **`OS_`** prefix is stripped when the action is loaded (`vdev_action_ini_parser`
  in `vdevd/action.c`); matching uses that bare name against **`vreq->params`**
  (`vdev_action_match()` in the same file).
- **Non-empty value** — the device event **must** contain that field **and** its
  value must match the string after **`=`**. Matching is **exact** (`strcmp`), or
  a list of exact alternatives separated by **`|`** (spaces around `|` are
  trimmed). This is **not** a regex (unlike **`path=`**). Examples:

  ```ini
  OS_SUBSYSTEM=block
  ```

  matches only when **`SUBSYSTEM=block`** is present on the request.

  ```ini
  OS_SUBSYSTEM=pci|block|usb
  ```

  matches when **`SUBSYSTEM`** is exactly **`pci`**, **`block`**, or **`usb`**.
  Each alternative is an exact string match (optional spaces around **`|`** are
  trimmed). It is **not** a POSIX ERE: do not write **`pci|block|usb`** inside
  **`path=`** and expect the same rules — **`path=`** uses **`regcomp`**, while
  **`OS_*`** uses **`vdev_action_os_value_match()`** in **`vdevd/action.c`**.

  Typical use — constrain initrd **`modprobe`** after a PCI bus **`trigger`**, so
  cascade uevents for drm/sound/… do not load modules (see
  **`debian/config/actions.initrd/002-modalias.act`**):

  ```ini
  [vdev-action]
  event=add
  OS_SUBSYSTEM=pci|block|usb
  OS_MODALIAS=
  helper=modprobe.sh
  daemonlet=true
  ```

  Without the **`OS_SUBSYSTEM=`** filter, **`OS_MODALIAS=`** alone would match
  every uevent that carries a modalias, including non-storage PCI children.

- **Empty value** — if you write **`OS_DEVPATH=`** (nothing after **`=`**, or
  only whitespace which the INI layer trims), the rule is: the field **must
  exist** on the device event, but its value is **not** constrained. That is how
  initrd actions such as **`001-sysfs-block.act`** express “only match events
  that already carry **`DEVPATH`**” without hard-coding a single path.

- If the device event **lacks** a field listed under **`OS_*`**, the action does
  **not** match (even when the action’s value is empty).

When a helper runs, the same kernel fields are also exported as environment
variables with a **`VDEV_OS_`** prefix (e.g. **`VDEV_OS_SUBSYSTEM`**) — see
`vdev_device_request_to_env()` in `vdevd/device.c`.

Common OS parameters:
- `OS_DEVPATH`: Device path in sysfs
- `OS_SUBSYSTEM`: Kernel subsystem (`block`, `input`, `usb`, `pci`, …). May be a
  single value or **`|`**-separated alternatives (`pci|block|usb`).
- `OS_DEVTYPE`: Device type (disk, partition, etc.); same `|` alternation allowed
- `OS_ID_*`: Various device identification fields; same `|` alternation allowed
- `OS_MODALIAS`: Present-with-any-value when written as **`OS_MODALIAS=`**; pair
  with **`OS_SUBSYSTEM=…`** when the action must not run for every modalias
  in a PCI trigger cascade

### `VAR_*` — helper configuration (never used for matching)

- Keys **`VAR_<NAME>`** are **not** consulted in **`vdev_action_match()`**. They
  are copied into the helper environment as **`VDEV_VAR_<NAME>`** so shell
  helpers can read site-specific defaults (owner, mode, sysfs root, …).
- **Empty value** — the variable is still defined; the helper typically sees
  **`VDEV_VAR_DISK_OWNER=`** and can apply shell defaults
  (**`${VDEV_VAR_DISK_OWNER:-root}`**, etc.).

Examples:

```ini
VAR_DISK_OWNER=root
VAR_DISK_GROUP=disk
VAR_DISK_MODE=0660
```

→ **`VDEV_VAR_DISK_OWNER`**, **`VDEV_VAR_DISK_GROUP`**, **`VDEV_VAR_DISK_MODE`**.

(See `vdev_device_request_to_env()` in `vdevd/device.c`.)

---

## `udisks2.act` + `udisks2.sh`

**Purpose:** For **`SUBSYSTEM=block`**, write **`$VDEV_METADATA/<basename>`** (default
**`udisks-props`**) containing lines such as **`UDISKS_IGNORE`**, **`UDISKS_NAME`**,
optional **`VDEV_FS_*`**, etc. The action name sorts **before** **`zzz-udev-compat`**,
so **`udisks2.sh`** has already updated metadata when **`zzz-udev-compat.act`**
runs. Inside **`udev-compat.sh`**, **`event_generate_text()`** (in
**`subr-event.sh`**) **cats** that file into the synthetic uevent text **before**
symlink/tag emission, so libudev-style consumers see the udisks-shaped properties.
This still happens **before** **`zzzz-os-queue-tracking.act`**, which is loaded
last and does not depend on the udisks props file.

**Why `event=any`:** Remove and change events should refresh or skip properties
consistently, not only on add.

**Sysfs root:** the helper-side C probes now receive the sysfs mountpoint as a
CLI argument from the calling shell wrappers (e.g. init script / helper script),
so **`udisks2.act`** no longer needs a dedicated **`VAR_UDISKS_SYSFS`** key.

**`VAR_UDISKS_PROPS_BASENAME`:** Optional. Defaults to **`udisks-props`** (basename
only, no **`/`**). **`udisks2.sh`** writes **`$VDEV_METADATA/<basename>`** and
records the same basename in **`$VDEV_METADATA/.vdev-udisks-props-basename`** so
**`event_generate_text()`** in **`subr-event.sh`** cats the matching file without
a second hard-coded name.

---

## `seat.act` + `seat.sh`

**Purpose:** Tag seat-related nodes and set group/mode so session software
(elogind, logind, etc.) can attach the active seat.

- **`dri/card*`** — tags **`seat`**, **`master-of-seat`**, **`graphics-primary`**;
  group **`video`**, mode **`0660`**; property **`ID_SEAT=seat0`**.
- **`dri/renderD*`** — tag **`seat`**, group **`render`** (fallback **`video`**), **`0660`**.
- **`input/event*`** — group **`input`**, **`0660`**.
- **`fb*`** — tag **`seat`**; **`master-of-seat`** on **`fb0`** or when hwdb sets
  **`ID_TAG_MASTER_OF_SEAT=1`**; group **`video`**, **`0660`**.

**`event=add`** — tags and permissions are applied when the node appears.

---

## `uaccess.act` + `uaccess.sh`

**Purpose:** Add the **`uaccess`** tag (for logind-style ACL management) and set
type-specific groups for common session hardware. On paths matched here, also invoke
**`elogind-uaccess-command`** when the node exists (vdev equivalent of udev
**`73-seat-late.rules`** **`RUN`** — vdev has no udev **`RUN`** hook).

**`snd/*`** devices are **not** matched by this action; use **`70-sound-uaccess.act`**
(same helper, dedicated path regex + **`OS_SUBSYSTEM=sound`**).

| Path prefix   | Group    |
|---------------|----------|
| `audio*`, …   | `audio`  |
| `video*`, `dri/*` | `video` |
| `hidraw*`, `usb/lp*` | `plugdev` |
| `input/event*` | `input` |
| `tty[0-9]+`   | (tag only; elogind applies session ACL) |

Mode **`0660`** when the node exists (except **`ttyN.act`** keeps **`0620`** when it
runs later in the chain). **`event=any`** (except **`remove`**) so **`change`** retriggers
ACLs after login. **`if_exists=run`** so phase-2 coldplug re-applies tags. **`VDEV_PATH`**
selects the devtmpfs path for the match.

---

## ALSA actions (`sound-*`, `70-sound-uaccess`, `78-sound-card*`, `90-alsa-restore`)

These actions mirror the udev rules shipped for **eudev** / **systemd-udev** on
**`SUBSYSTEM=sound`**. They are installed under **`debian/config/actions.early/`** for
**early hotplug** (**`vdevd.early.conf`** → **`actions.early`**; **`sound`** is listed in
**`vdevd.globals.json`** → **`profiles.early.subsystems`**) and under **`example/actions/`**
for long-running **`vdevd.conf`**. Reference copies of the udev rules:
**`~/UNI-JSON/udev-rules/`**.

### End-to-end flow

```text
kernel uevent (sound)
  → sound-alsa-control.act   … symlinks, audio group, 0660
  → 70-sound-uaccess.act     … uaccess tag, elogind-uaccess-command, audio-acl.sh
  → 78-sound-card-control.act … controlC* → card change uevent
  → 78-sound-card.act        … card* change → SOUND_* / ID_* properties
  → 90-alsa-restore.act      … alsactl restore (+ unmute analog)
  → seat.act (snd/card*)     … seat tag (71-seat.rules partial)
  → zzz-udev-compat.act      … /run/udev/data with E:DEVNAME, G:uaccess, …
```

**elogind** reads **`/run/udev/data/c116:MIN`** at session start. Without **`E:DEVNAME=/dev/snd/...`**
(before **`udev-compat.sh`** was extended), sandboxed browsers (**`pulse-rust`**) could stay
silent while **`aplay`** (group **`audio`**) worked.

There is **no** separate **`72-sound-acl.act`**: **`70-sound-uaccess.act`** + **`uaccess.sh`**
already call **`audio-acl.sh`** after **`elogind-uaccess-command`**.

### `sound-alsa-control.act` + `sound.sh`

**udev:** **`60-persistent-alsa.rules`**

```ini
[vdev-action]
event=any
path=^snd/.+$
OS_SUBSYSTEM=sound
helper=sound.sh
VAR_SOUND_OWNER=root
VAR_SOUND_GROUP=audio
VAR_SOUND_MODE=0660
if_exists=run
daemonlet=true
```

- **`add`:** **`snd/by-path`** / **`snd/by-id`** symlinks for USB and persistent paths;
  **`vdev_permissions`** on the node (**`0660`**, group **`audio`**).
- **`remove`:** **`vdev_cleanup`** symlinks.
- **`dev-setup.sh`** creates static **`snd/seq`** and **`snd/timer`** from
  **`modules.devname`** when missing.

### `70-sound-uaccess.act` + `uaccess.sh` + `audio-acl.sh`

**udev:** **`70-uaccess.rules`** (**`TAG+="uaccess"`** on **`SUBSYSTEM=sound`**) and
**`73-seat-late.rules`** (**`RUN+=elogind-uaccess-command`**).

```ini
[vdev-action]
event=any
path=^snd/.+$
OS_SUBSYSTEM=sound
helper=uaccess.sh
daemonlet=true
if_exists=run
```

**`uaccess.sh`** on **`snd/*`:**

1. Tag **`uaccess`**, property **`VDEV_SEAT=seat0`** (→ **`E:ID_SEAT`** in udev-compat).
2. Group **`audio`**, mode **`0660`**.
3. **`/usr/libexec/elogind-uaccess-command`** **`$node`** **`seat0`**.
4. **`audio-acl.sh`** — **`setfacl u:$session_user:rw`** when **`loginctl`** reports an
   active seat session (fallback if step 3 no-ops under vdev).

Requires package **`acl`** (**`setfacl`**) for the fallback path.

### `78-sound-card-control.act` + `78-sound-card.act` + `sound-card.sh`

**udev:** **`78-sound-card.rules`**

| Action | Match | Event | Role |
|--------|-------|-------|------|
| **`78-sound-card-control.act`** | **`^snd/controlC[0-9]+$`** | **`any`** | On **`controlC*`** **`add`/`change`**, write **`change`** to the parent **`cardN`** sysfs uevent |
| **`78-sound-card.act`** | ( **`OS_SUBSYSTEM=sound`**, card sysfs path ) | **`change`** | Set **`SOUND_*`**, **`ID_*`**, USB/PCI properties on the card |

Properties land in **`$VDEV_METADATA/properties`** and appear as **`E:`** lines after
**`zzz-udev-compat.act`**.

### `90-alsa-restore.act` + `alsa-restore.sh`

**udev:** **`90-alsa-restore.rules`**

```ini
[vdev-action]
event=any
path=^snd/controlC[0-9]+$
OS_SUBSYSTEM=sound
helper=alsa-restore.sh
if_exists=run
```

Runs **`/usr/sbin/alsactl restore`** for the card number derived from **`controlC*`**. After
restore, unmutes **Master** / **Speaker** on **HDA analog** cards when the saved state had
**`Master Playback Switch=false`** (common after a muted session).

Runs synchronously (**`async`** is not set — restore must finish before clients open PCM).

### `sound-audio.act` (legacy)

Matches **`path=^audio$`** (OSS **`/dev/audio`**) with **`permissions.sh`**. Kept in
**`example/actions/`** for parity with old udev snippets; typical desktop stacks use
**`snd/*`** only.

### `seat.act` and sound

**`seat.act`** includes **`snd/card[0-9]+`** so **`71-seat.rules`**-style **`seat`** tags
apply to ALSA card sysfs nodes (not every **`pcmC*`** leaf).

### `zzz-udev-compat.act` + core **`E:DEVNAME`**

**`udev-compat.sh`** writes **`/run/udev/data/cMAJ:MIN`**. Besides metadata **`properties`**
and **`tags/`**, **`udev_enumerate_core_properties()`** adds:

- **`E:DEVNAME=/dev/snd/...`**
- **`E:SUBSYSTEM=sound`**
- **`E:DEVPATH`**, **`E:MAJOR`**, **`E:MINOR`**

**`check-seat-udev.sh`** warns if **`G:uaccess`** **`sound`** records lack
**`E:DEVNAME=/dev/snd/`**.

### Host configuration (not vdev actions)

**Card index** — **`/etc/modprobe.d/alsa.conf`** example:

```text
options snd-hda-intel id=NVidia index=0
options snd-hda-intel id=PCH index=1
```

**Default route** — **`/etc/asound.conf`** (package: **`debian/vdev-asound-analog.conf`**):

```text
pcm.!default { type plug; slave.pcm "hw:PCH,0"; }
ctl.!default { type hw; card PCH; }
```

Full verification steps: **`how-to-test.md`** (“ALSA and desktop browser audio”).

### Test helpers

| Script | Purpose |
|--------|---------|
| **`example/test-alsa-actions.sh`** | Exercise **`sound.sh`**, **`sound-card.sh`**, **`alsa-restore.sh`** against live **`/sys/class/sound`** |
| **`example/test-browser-audio.sh`** | ACL, udev DB sample line, **`ffplay`** smoke test |

---

## `kbd-default.act` + `keyboard-default.sh`

**Purpose:** Apply console keyboard defaults from **`/etc/default/keyboard`**
when the kernel keyboard node appears.

```ini
[vdev-action]
event=add
path=^kbd$
helper=keyboard-default.sh
daemonlet=false
```

### Why `path=^kbd$` (and what “KERNEL” means here)

In udev terms this is equivalent to **`KERNEL=="kbd"`**. The “KERNEL” value is
the kernel node name (basename of the device node path). In vdev that value is
matched through **`VDEV_PATH`** using **`path=`** regex, so exact **`kbd`** is
written as **`^kbd$`**.

### Helper behavior

`keyboard-default.sh` runs only for `event=add` and `VDEV_PATH=kbd`.

- If `/etc/default/keyboard` is missing, it exits quietly.
- If `setupcon` exists (`/bin/setupcon` or `/usr/bin/setupcon`), it runs
  `setupcon --force` to apply that file.
- If `setupcon` is unavailable, it logs a warning and leaves the system
  unchanged.

---

## `zzzz-os-queue-tracking.act` + `os-queue-tracking.sh`

This rule is named with **`zzzz-…`** so it sorts **after** almost everything else
(including **`zzz-udev-compat.act`**), which matches what you see in **`vdevd`**
logs: queue-tracking runs **last** for each matching device event.

**INI (typical):**

```ini
[vdev-action]
event=any
queue_tracking_handler=1
helper=os-queue-tracking.sh
daemonlet=false
```

**Role — coldplug / phased boot bookkeeping (split concerns):**

- **`vdevd`** — For **`coldplug_only`** sysfs walks, attaches a synthetic
  **`QUEUE_TRACKING_FLAG`** from **`[vdev-config] queue_tracking_flag`** on the last
  device (e.g. Debian **`vdevd.1.conf`** uses **`2`**). Listeners are seeded by
  **`vdevadm trigger`**, not by C subsystem queues.

- **Scripts only** — JSON **`stamp`** filenames plus **`globals["states-stamp-dir"]`**
  are not read by **`vdevd`** for device logic. They let SysV **`jq`** and
  **`os-queue-tracking.sh`** map **`VDEV_CONFIG_FILE`** to **`.ready`** paths.
  Absolute values **`1`** / **`2`** touch the stamp; **`3`** only **`SIGTERM`**s
  **`$PPID`**. With **`VDEV_COLDPLUG_ONLY=1`**, the helper always signals the parent
  after handling the flag.

`vdev_device_request_to_env` (vdevd/device.c) injects **VDEV_GLOBALS_JSON** into the
helper environment (async subprocesses use `clearenv` before `exec`).

Boot pacing: **`vdevd.early`** → **`vdevadm control --promote`** → **`vdev.ready`**
(coldplug script) → optional terminal **`vdevd.1 -1`** / **`--sysfs`**. **`vdevd`**
keys off INI **`config`** and **`queue_tracking_flag`**, not JSON stamp strings.

### Why stamps via this action instead of polling inside `linux_events.inc`

The same bookkeeping — **“this sysfs coldplug queue is drained”** and **“tear down
this `vdevd` instance”** — could, in principle, be implemented **inside**
**`vdevd/os/linux_events.inc`**. That path is **not** taken here on purpose.
**`QUEUE_TRACKING_FLAG`** is aimed at **init scripts** (handoff after promote /
optional terminal **`vdevd.1`**), **not** at teaching **`vdevd`** new internal state
through **libudev-compat**. Boot orchestration needs a **stable signal outside the
process** — hence **`.ready` stamp files** (and **`vdev.ready`** from coldplug after
**`control --promote`**).

**`zzzz-os-queue-tracking.act`** therefore keeps **`vdevd`**’s event loop on **kernel
sysfs / netlink** semantics and stays **as independent as possible of libudev-compat**.
**`vdevd`** still marks the last queued device with **`QUEUE_TRACKING_FLAG`**; only
**`queue_tracking_handler`** actions run; **`os-queue-tracking.sh`** touches the
JSON-resolved stamp and may **`SIGTERM`** the parent.

### `queue_tracking_flag` vs `coldplug_only` — same profile, distinct roles

Treat **`queue_tracking_flag` ≠ 0** as an **index** that selects bookkeeping when the
**last uevent off a coldplug-only sysfs walk** is processed (what **`os-queue-tracking.sh`**
does on **`QUEUE_TRACKING_FLAG`**: **`touch`** stamps for **`1`**/**`2`**, **`SIGTERM`**
parent for **`3`** / **`VDEV_COLDPLUG_ONLY`**, etc.). It is **not** the flag that implies
listener vs oneshot: **`coldplug_only=1`** is for a **short-lived** slice (pkg
**`vdevd.1.conf`** with **`queue_tracking_flag=2`**): after that queue drains,
**`os-queue-tracking.sh`** touches **`vdevd-1-coldplug.ready`** and **`SIGTERM`**s the
parent so the oneshot exits without fighting the long-running hotplug daemon.
Foreground tests of the same profile: **Ctrl+C** (**`SIGINT`**) also exits the poll
loop (no control socket on **`coldplug_only`**). Listener PID discovery uses
**`vdevadm control --print-pid`**.

To bolt on behaviour: pick a fresh integer **`>` 0 in **`[vdev-config] queue_tracking_flag=`**,
add a matching **`case`** arm in **`os-queue-tracking.sh`**, keep
**`queue_tracking_handler=1`** on **`zzzz-os-queue-tracking.act`**.

So **positive flags** skew toward **run-once sequencer hooks** (**"done draining this phase's queue"**) more naturally than toward **"coldplug_only instance"**, which separately means **"don't keep netlink (etc.) alive in this daemon"**.

### Why **QUEUE_TRACKING_FLAG** can be **negative** (`-1`, legacy `-2/-3`)

Positive flags mark the **last** item of a coldplug queue so the stamp (or
**SIGTERM**) runs exactly once. If vdevd hits a **discard / recovery** path
while that flag is still set — for example `vdev_action_create_path()` fails
for an ADD with a positive marker, or there is no usable path — the daemon
**negates** the flag in the in-memory request (`vdev_device_request_negate_queue_tracking_flag()`
in vdevd/device.c) so normal actions do **not** keep trying to build a broken
device. Matching then uses `vdev_action_negative_queue_tracking_excludes_non_handler()`
in vdevd/action.c: if `QUEUE_TRACKING_FLAG < 0`, most actions are
skipped, but actions with **queue_tracking_handler=1** (this one) still
run. **os-queue-tracking.sh** treats negative markers as valid end-of-queue
signals, so stamps / parent `SIGTERM` still fire and coldplug
orchestration does not hang waiting for a stamp that would never appear.

## Best Practices

1. **Use Daemonlets for Long-running Processes**: Set `daemonlet=true` for actions that need to maintain state or handle multiple events efficiently.

2. **Handle Both Add and Remove Events**: Always include removal logic to clean up resources properly.

3. **Use Helper Scripts for Complex Logic**: Prefer helper scripts over inline commands for anything beyond simple operations.

4. **Follow Naming Conventions**: Use descriptive names for action files and helpers.

5. **Set Appropriate Permissions**: Ensure helper scripts have proper execute permissions.

6. **Use Common Functions**: Leverage functions from `subr.sh` for consistency.

7. **Test Thoroughly**: Test both addition and removal scenarios.

## Advanced Features

### Queue Tracking

Actions can be configured to handle queue tracking scenarios:

```ini
queue_tracking_handler=true
```

This allows the action to run even when `QUEUE_TRACKING_FLAG` is negative, which
is useful for queue-tracking recovery handling.

### Asynchronous Execution

For long-running operations that shouldn't block device processing:

```ini
async=true
```

### `if_exists` in practice

`if_exists` is mainly relevant on `add` flows when vdev detects that the target
node already exists. In this repository, **`sound-alsa-control.act`** is the
explicit example using:

```ini
if_exists=run
```

That is useful for actions whose side effects are still desirable on re-add /
already-present devices (permissions, symlinks, metadata refresh, compatibility
properties).

Additional metadata-oriented rules that now explicitly set **`if_exists=run`**
for intent clarity:

- **`udisks2.act`** (metadata generation under `$VDEV_METADATA`)
- **`zzz-udev-compat.act`** (compat event/property refresh path)

This makes the policy explicit for these rules if `if_exists` handling is
tightened in the future.

### Conditional Execution

Use OS parameters for fine-grained matching:

```ini
OS_SUBSYSTEM=usb
OS_ID_VENDOR_ID=046d
OS_ID_MODEL_ID=c52b
```

### Multiple Actions

Create multiple action files with different priorities (lexicographic filename
order) to handle different aspects of device setup.

## Troubleshooting

1. **Check Action Syntax**: Verify INI format and required fields
2. **Verify Helper Permissions**: Ensure helper scripts are executable
3. **Check Logs**: Look in vdev logs for error messages
4. **Test Manually**: Run helper scripts manually with appropriate environment variables
5. **Validate Regex**: Test path matching patterns

## Reference Implementation

See the example actions in `example/actions/` and helpers in `vdevd/helpers/LINUX/` for complete working examples.

---

## See also

- `vdevd/action.h` — field names and `VAR_` / `OS_` prefixes.
- `how-to-test.md` — overview of the action pipeline.
- `example/vdevd.globals.json` — user-editable profile/state names and subsystem
  queues consumed by `vdev_state.globals` (with runtime fallback defaults).
- `vdevd/helpers/LINUX/subr-event.sh` — merges the udisks props file (basename from
  **`udisks2.sh`** / **`.vdev-udisks-props-basename`**, default **`udisks-props`**).
- `vdevd/helpers/LINUX/keyboard-default.sh` — applies `/etc/default/keyboard` when
  the `kbd` node appears (`path=^kbd$`, udev `KERNEL=="kbd"` equivalent).
- `vdevd/helpers/LINUX/os-queue-tracking.sh` — stamp files and **`SIGTERM`** for
  **`zzzz-os-queue-tracking.act`**.
