StreamController 1.5.0-beta.15, pulsectl 24.12.0, libpulse 17.0, pipewire-pulse 1.6.8, Python 3.14 (GIL build), Arch.
With the stock VolumeMixer page (4x VolumeUp, 4x VolumeDown, 4x VolumeMute, MoveLeft, MoveRight, Exit), StreamController aborts inside libpulse (pstream queue assertion) minutes after the page loads. Three cores in one evening; timing-dependent, so it recurs irregularly.
Mechanism (from the source, not inferred from the stack):
DeckController.py spawns a new thread per tick, per input (threading.Thread(target=self.own_actions_tick, ...)).
- Twelve of the fifteen actions call
self.plugin_base.pulse.sink_input_list() from on_tick().
- All twelve share the single
pulsectl.Pulse("stream-controller", threading_lock=True) created in main.py:174.
- In pulsectl 24.12.0 that lock is only held inside
_pulse_loop(), i.e. around mainloop iteration. The command submission in _pulse_get_list._wrapper_method calls pa_context_get_sink_input_info_list() outside any lock.
- ctypes releases the GIL for foreign calls, so a dozen OS threads push onto the same
pa_context pstream queue concurrently; the queue corrupts and libpulse's own assertion aborts the process.
threading_lock=True looks like it makes this safe; it does not.
When it fires: ticking is suspended while the screensaver shows, so unlocking restarts all twelve tick threads at once — both observed aborts came 30 s to 2 min after an unlock.
Fix options: a real lock around sink_input_list / mute / volume_set_all_chans, or query the sink-input list once per tick and share the result across the actions instead of twelve times.
StreamController 1.5.0-beta.15, pulsectl 24.12.0, libpulse 17.0, pipewire-pulse 1.6.8, Python 3.14 (GIL build), Arch.
With the stock VolumeMixer page (4x VolumeUp, 4x VolumeDown, 4x VolumeMute, MoveLeft, MoveRight, Exit), StreamController aborts inside libpulse (pstream queue assertion) minutes after the page loads. Three cores in one evening; timing-dependent, so it recurs irregularly.
Mechanism (from the source, not inferred from the stack):
DeckController.pyspawns a new thread per tick, per input (threading.Thread(target=self.own_actions_tick, ...)).self.plugin_base.pulse.sink_input_list()fromon_tick().pulsectl.Pulse("stream-controller", threading_lock=True)created inmain.py:174._pulse_loop(), i.e. around mainloop iteration. The command submission in_pulse_get_list._wrapper_methodcallspa_context_get_sink_input_info_list()outside any lock.pa_contextpstream queue concurrently; the queue corrupts and libpulse's own assertion aborts the process.threading_lock=Truelooks like it makes this safe; it does not.When it fires: ticking is suspended while the screensaver shows, so unlocking restarts all twelve tick threads at once — both observed aborts came 30 s to 2 min after an unlock.
Fix options: a real lock around
sink_input_list/mute/volume_set_all_chans, or query the sink-input list once per tick and share the result across the actions instead of twelve times.