PulseAudio with changing machine-id
Mon, 20 May 2024 00:01:07 -0400

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][1]
for the file name of one of its databases:

    $ 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`][2]:

    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`][3]:

    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][4]: 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`][5]
    is typically used to initialize `/etc/machine-id` at install
    time (this is what [Debian][6] [does][7], 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][8] using [`dbus-uuidgen`][9].  See also
    [`dbus.README.Devuan`][10].

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

[1]: https://cgit.freedesktop.org/pulseaudio/pulseaudio
[2]: https://cgit.freedesktop.org/pulseaudio/pulseaudio/tree/src/pulsecore/database.c?id=6c77b0191a6be390cc1cef2f7da7ba202ab86a92#n34
[3]: https://cgit.freedesktop.org/pulseaudio/pulseaudio/tree/src/pulsecore/core-util.c?id=6c77b0191a6be390cc1cef2f7da7ba202ab86a92#n3211
[4]: https://www.freedesktop.org/software/systemd/man/latest/machine-id.html
[5]: https://www.freedesktop.org/software/systemd/man/latest/systemd-machine-id-setup.html
[6]: https://wiki.debian.org/MachineId
[7]: https://salsa.debian.org/systemd-team/systemd/-/blob/30c77a7332b5f44cbade27155c0b8e816a75ae7f/debian/systemd.postinst#L42
[8]: https://git.devuan.org/devuan/dbus/src/commit/53425f6f4ab4f9b1cc4a6593dfe5618476c13e75/debian/dbus-daemon.postinst#L6
[9]: https://dbus.freedesktop.org/doc/dbus-uuidgen.1.html
[10]: https://git.devuan.org/devuan/dbus/src/commit/53425f6f4ab4f9b1cc4a6593dfe5618476c13e75/debian/dbus.README.Devuan
[11]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html