thinkcenter-audio: harden — linger-first + wait runtime dir + rescue fallback for user units, apt retries, user/uid asserts

This commit is contained in:
Samuel James 2026-09-03 13:37:59 +00:00
parent aca4801744
commit 6fa9505f5a

View File

@ -94,25 +94,44 @@
that: inventory_hostname == 'thinkcenter' that: inventory_hostname == 'thinkcenter'
fail_msg: "Refusing to run — this play is scoped to thinkcenter only." fail_msg: "Refusing to run — this play is scoped to thinkcenter only."
- name: "[safety] Assert audio_user resolved from Infisical"
ansible.builtin.assert:
that:
- audio_user is defined
- audio_user | length > 0
fail_msg: "audio_user is empty — THINKCENTER_USER not read from Infisical /lc-hosts."
- name: "[facts] Resolve audio_user UID (for XDG_RUNTIME_DIR)" - name: "[facts] Resolve audio_user UID (for XDG_RUNTIME_DIR)"
ansible.builtin.getent: ansible.builtin.getent:
database: passwd database: passwd
key: "{{ audio_user }}" key: "{{ audio_user }}"
- name: "[facts] Assert the user actually exists on the host"
ansible.builtin.assert:
that: audio_user in ansible_facts.getent_passwd
fail_msg: "User '{{ audio_user }}' does not exist on {{ inventory_hostname }}."
- name: "[facts] Set audio_uid" - name: "[facts] Set audio_uid"
ansible.builtin.set_fact: ansible.builtin.set_fact:
audio_uid: "{{ ansible_facts.getent_passwd[audio_user][1] }}" audio_uid: "{{ ansible_facts.getent_passwd[audio_user][1] }}"
- name: "[apt] Update cache" - name: "[apt] Update cache (retry on transient network/lock)"
ansible.builtin.apt: ansible.builtin.apt:
update_cache: true update_cache: true
cache_valid_time: 3600 cache_valid_time: 3600
register: apt_cache
until: apt_cache is succeeded
retries: 3
delay: 10
- name: "[apt] Install unified PipeWire audio stack + qpwgraph" - name: "[apt] Install unified PipeWire audio stack + qpwgraph (retry on dpkg lock)"
ansible.builtin.apt: ansible.builtin.apt:
name: "{{ audio_packages }}" name: "{{ audio_packages }}"
state: present state: present
register: pkg_install register: pkg_install
until: pkg_install is succeeded
retries: 3
delay: 15
- name: "[conflict] Detect a STANDALONE pulseaudio daemon package (not the pw-pulse shim)" - name: "[conflict] Detect a STANDALONE pulseaudio daemon package (not the pw-pulse shim)"
ansible.builtin.shell: "dpkg -l pulseaudio 2>/dev/null | awk '/^ii/{print $2}'" ansible.builtin.shell: "dpkg -l pulseaudio 2>/dev/null | awk '/^ii/{print $2}'"
@ -134,27 +153,59 @@
register: mask_pa register: mask_pa
changed_when: mask_pa.rc == 0 changed_when: mask_pa.rc == 0
- name: "[services] Enable PipeWire user services for {{ audio_user }} (lingering + enable)" # --- User-session services: made robust ---------------------------------
# systemctl --user needs a live user runtime dir + D-Bus. Enabling linger
# FIRST makes systemd create a persistent /run/user/<uid> even with no GUI
# login, so this works whether or not steve is currently logged in.
- name: "[services] Enable lingering for {{ audio_user }} (persistent user manager)"
ansible.builtin.command: "loginctl enable-linger {{ audio_user }}" ansible.builtin.command: "loginctl enable-linger {{ audio_user }}"
register: linger register: linger
changed_when: "'already' not in (linger.stderr | default(''))" changed_when: "linger.rc == 0 and 'already enabled' not in (linger.stderr | default(''))"
failed_when: false failed_when: linger.rc != 0 and 'already enabled' not in (linger.stderr | default('') + linger.stdout | default(''))
- name: "[services] Enable pipewire / pipewire-pulse / wireplumber for the user" - name: "[services] Wait for user runtime dir /run/user/{{ audio_uid }} to exist"
become: true ansible.builtin.stat:
become_user: "{{ audio_user }}" path: "/run/user/{{ audio_uid }}"
ansible.builtin.systemd_service: register: runtime_dir
name: "{{ item }}" until: runtime_dir.stat.exists
scope: user retries: 6
enabled: true delay: 5
loop:
- pipewire.socket - name: "[services] Enable PipeWire user units (with rescue if session bus is not ready)"
- pipewire-pulse.socket block:
- wireplumber.service - name: "[services] Enable pipewire / pipewire-pulse / wireplumber for the user"
environment: become: true
XDG_RUNTIME_DIR: "/run/user/{{ audio_uid }}" become_user: "{{ audio_user }}"
failed_when: false ansible.builtin.systemd_service:
register: user_services name: "{{ item }}"
scope: user
enabled: true
loop:
- pipewire.socket
- pipewire-pulse.socket
- wireplumber.service
environment:
XDG_RUNTIME_DIR: "/run/user/{{ audio_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ audio_uid }}/bus"
register: user_services
rescue:
- name: "[services] FALLBACK — enable units via systemctl --user by uid (no D-Bus dependency)"
become: true
become_user: "{{ audio_user }}"
ansible.builtin.command: "systemctl --user enable {{ item }}"
loop:
- pipewire.socket
- pipewire-pulse.socket
- wireplumber.service
environment:
XDG_RUNTIME_DIR: "/run/user/{{ audio_uid }}"
register: user_services_fallback
changed_when: "'Created symlink' in (user_services_fallback.stderr | default(''))"
failed_when: false
- name: "[services] Note the fallback path was used"
ansible.builtin.debug:
msg: "User D-Bus not ready; enabled PipeWire units via the systemctl fallback. They will start on next login/linger."
- name: "[verify] Which server does pactl report?" - name: "[verify] Which server does pactl report?"
become: true become: true
@ -162,6 +213,7 @@
ansible.builtin.command: pactl info ansible.builtin.command: pactl info
environment: environment:
XDG_RUNTIME_DIR: "/run/user/{{ audio_uid }}" XDG_RUNTIME_DIR: "/run/user/{{ audio_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ audio_uid }}/bus"
register: pactl_info register: pactl_info
changed_when: false changed_when: false
failed_when: false failed_when: false