diff --git a/life-church/playbooks/thinkcenter-audio.yml b/life-church/playbooks/thinkcenter-audio.yml index 9d5c483..74b91ba 100644 --- a/life-church/playbooks/thinkcenter-audio.yml +++ b/life-church/playbooks/thinkcenter-audio.yml @@ -94,25 +94,44 @@ that: inventory_hostname == 'thinkcenter' 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)" ansible.builtin.getent: database: passwd 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" ansible.builtin.set_fact: 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: update_cache: true 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: name: "{{ audio_packages }}" state: present 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)" ansible.builtin.shell: "dpkg -l pulseaudio 2>/dev/null | awk '/^ii/{print $2}'" @@ -134,27 +153,59 @@ register: mask_pa 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/ 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 }}" register: linger - changed_when: "'already' not in (linger.stderr | default(''))" - failed_when: false + changed_when: "linger.rc == 0 and 'already enabled' not in (linger.stderr | default(''))" + 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" - become: true - become_user: "{{ audio_user }}" - ansible.builtin.systemd_service: - name: "{{ item }}" - scope: user - enabled: true - loop: - - pipewire.socket - - pipewire-pulse.socket - - wireplumber.service - environment: - XDG_RUNTIME_DIR: "/run/user/{{ audio_uid }}" - failed_when: false - register: user_services + - name: "[services] Wait for user runtime dir /run/user/{{ audio_uid }} to exist" + ansible.builtin.stat: + path: "/run/user/{{ audio_uid }}" + register: runtime_dir + until: runtime_dir.stat.exists + retries: 6 + delay: 5 + + - name: "[services] Enable PipeWire user units (with rescue if session bus is not ready)" + block: + - name: "[services] Enable pipewire / pipewire-pulse / wireplumber for the user" + become: true + become_user: "{{ audio_user }}" + ansible.builtin.systemd_service: + 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?" become: true @@ -162,6 +213,7 @@ ansible.builtin.command: pactl info environment: XDG_RUNTIME_DIR: "/run/user/{{ audio_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ audio_uid }}/bus" register: pactl_info changed_when: false failed_when: false