PulseAudio with changing machine-id

Mon, 20 May 2024 00:01:07 -0400

Cross-posted from my personal web/gopher log:

https://kelar.org/~bandali/blog/pacify.html

gopher://kelar.org/0/~bandali/phlog/pacify.txt

I've been using Devuan GNU/Linux on several of my machines in the recent months. On one of these machines, I have a somewhat elaborate audio setup with multiple input and output devices plugged in. Not long after setting up Devuan unstable, I noticed PulseAudio ('pulse' for short from here on) would forget my configurations after each reboot. I would set the default input and output devices and their volumes to my liking, only for pulse to forget it all on the next boot. Interestingly, I'd never run into this issue on Debian or its other derivatives like Trisquel over the years.

To try and figure out what was going on, I set out by checking if I could spot any suspicious-looking configuration values for pulse itself or any of its modules. From a cursory look, the `/etc/pulse/default.pa` global configuration file looked familiar and reasonable enough. Next, I thought I would check if Devuan's `pulseaudio` package was forked from Debian (in case their default configuration had any problematic bits that Debian's didn't), or if they shipped Debian's as-is. It was the latter: Devuan was shipping Debian's `pulseaudio` directly, with no Devuan-specific changes. This lead me to think that this problematic behaviour of pulse may be due to a bad interaction with another part of the system, one where Devuan does differ from Debian.

At this point, without any concrete clues to go on by, I decided to check whether pulse was saving my configurations at all, and if so, what was happening on each boot that caused it to "forget" them. So I checked the `~/.config/pulse` user configurations directory and the "what" became somewhat apparent: there were multiple sets of the pulse configuration and database files, but with different prefixes. Something like this:

$ ls ~/.config/pulse/
19b7ae2816d8f92780787a62680a53c8-card-database.tdb
19b7ae2816d8f92780787a62680a53c8-default-sink
19b7ae2816d8f92780787a62680a53c8-default-source
19b7ae2816d8f92780787a62680a53c8-device-volumes.tdb
19b7ae2816d8f92780787a62680a53c8-stream-volumes.tdb
bbe80bf75c4b6ac9a09693be6bf36645-card-database.tdb
bbe80bf75c4b6ac9a09693be6bf36645-default-sink
bbe80bf75c4b6ac9a09693be6bf36645-default-source
bbe80bf75c4b6ac9a09693be6bf36645-device-volumes.tdb
bbe80bf75c4b6ac9a09693be6bf36645-stream-volumes.tdb
cookie

I tried rebooting the machine once again, and sure enough, there was a new set of files with a new prefix, and pulse no longer touched the previous ones. So, whatever these random-looking 32-character hex numbers were, a new one was generated on each boot, and because pulse used them in its per-user configuration and database file names, it could not find the previous files at the next boot.

The next logical question was: just what are these random-looking numbers, and where does pulse get them from? So I went digging in pulse's sources. I started by searching the [pulse sources] for the file name of one of its databases:

pulse sources

$ grep -rn stream-volumes
src/modules/module-stream-restore.c:2386:    if (!(u->database = pa_database_open(state_path, "stream-volumes", true, true))) {

Looking at the definition of [`pa_database_open`]:

`pa_database_open`

pa_database* pa_database_open(const char *path, const char *fn, bool prependmid, bool for_write) {

    const char *filename_suffix = pa_database_get_filename_suffix();

    /* [... omitted for brevity ...] */
    if (prependmid && !(machine_id = pa_machine_id())) {
        return NULL;
    }

    /* Database file name starts with ${machine_id}-${fn} */
    if (machine_id)
        filename_prefix = pa_sprintf_malloc("%s-%s", machine_id, fn);
    else
        filename_prefix = pa_xstrdup(fn);
    /* [... omitted for brevity ...] */
}

And [`pa_machine_id`]:

`pa_machine_id`

char *pa_machine_id(void) {
    FILE *f;
    /* [... omitted for brevity ...] */
    if ((f = pa_fopen_cloexec(PA_MACHINE_ID, "r")) ||
        (f = pa_fopen_cloexec(PA_MACHINE_ID_FALLBACK, "r")) ||
#if !defined(OS_IS_WIN32)
        (f = pa_fopen_cloexec("/etc/machine-id", "r")) ||
        (f = pa_fopen_cloexec("/var/lib/dbus/machine-id", "r"))
#else
        false
#endif
        ) {
        /* [... omitted for brevity ...] */
    }
    /* [... omitted for brevity ...] */
}

AHA!! So this is where the prefix for the above database files come from; it's the machine-id[^1]. We see that pulse tries to read it from `/etc/machine-id`, which is absent in Devuan, and if that fails it tries `/var/lib/dbus/machine-id` next, which does exist in Devuan.

Having found what the file name prefix is and where it comes from, we can try to work around the issue. What came to my mind was to store the current machine-id in some file, and on the next boot use it to find and rename the right set of pulse configuration and database files to use the newly generated machine-id.

So, I wrote a tiny POSIX shell script to be run during startup that does exactly that. It expects the previous machine-id in `$XDG_CACHE_HOME/tmp-prevmid`, and uses it to match and rename the files in `$XDG_CONFIG_HOME/pulse` that have that prefix, so that pulse could find its own configuration and database files again[^2]. The script does the same for libcanberra's event sound cache database as well (another project with the same author as pulse).

Here is the script, which I named `pacify` (PAcify), in its entirety:

#!/bin/sh
XDG_CACHE_HOME="${XDG_CACHE_HOME=:-$HOME/.cache}"
XDG_CONFIG_HOME="${XDG_CONFIG_HOME=:-$HOME/.config}"
cur_id_path=/var/lib/dbus/machine-id
prev_id_path="$XDG_CACHE_HOME/tmp-prevmid"
cur_id="$(cat $cur_id_path)"
prev_id="$(cat $prev_id_path)"

if [ -d "$XDG_CONFIG_HOME/pulse" ] && [ -s "$prev_id_path" ]; then
    for f in $XDG_CONFIG_HOME/pulse/$prev_id-* \
                 $XDG_CACHE_HOME/event-sound-cache.tdb.$prev_id.*; do
        fnew="$(echo $f | sed "s/$prev_id/$cur_id/")"
        mv $f $fnew
    done
fi

cp -p $cur_id_path $prev_id_path

I disclaim any rights to the above script. You're welcome to treat is as public domain, and do with it as you wish.

Take care, and so long for now.

[^1]: Some background on [machine-id]: the concept originated in D-Bus, but has since made its way into systemd (of course). On systems that run systemd, [`systemd-machine-id-setup`] is typically used to initialize `/etc/machine-id` at install time (this is what [Debian] [does], for instance). Then, `/usr/lib/tmpfiles.d/dbus.conf` from the `dbus` package is used to make `/var/lib/dbus/machine-id` a symlink to `/etc/machine-id`. Devuan systems, not using systemd, do not have a `/etc/machine-id` file, and Devuan's `dbus` package does not include `/usr/lib/tmpfiles.d/dbus.conf`. Instead, in Devuan `/var/lib/dbus/machine-id` is a regular file [generated in the post-install hook of the `dbus` package] using [`dbus-uuidgen`]. See also [`dbus.README.Devuan`].

machine-id

`systemd-machine-id-setup`

Debian

does

generated in the post-install hook of the `dbus` package

`dbus-uuidgen`

`dbus.README.Devuan`

[^2]: `$XDG_CACHE_HOME` usually defaults to `~/.cache`, and `$XDG_CONFIG_HOME` to `~/.config`. See the [XDG Base Directory Specification] for more details.

XDG Base Directory Specification