fix: kill alsa_in in stop_jack; verify jackd matches config device
CI / test (push) Has been cancelled

Two-part fix for the rogue jackd race:

1. stop_jack() now kills alsa_in (from bt-a2dp bridge). When JACK
   dies, alsa_in auto-spawns a rogue jackd -T -ndefault -d alsa
   that satisfies _jack_is_operational() but is the wrong server.

2. _jack_is_operational() now paired with _jack_matches_config()
   which verifies the running jackd's /proc/cmdline contains the
   expected ALSA device string (e.g. hw:USB,0). A rogue jackd
   without this won't pass the readiness check.

Also added _jack_matches_config() helper that scans /proc/*/cmdline.
This commit is contained in:
2026-06-18 18:10:40 -04:00
parent f1c6f64dc8
commit 017a74a414
+43 -3
View File
@@ -294,8 +294,9 @@ class AudioSystem:
# IMPORTANT: On JACK 1.9.22 there is no /dev/shm/jack_default_0_0
# socket. Instead, verify via jack_lsp which tests actual IPC.
if _jack_is_running():
# Verify JACK is actually accepting connections
if _jack_is_operational():
# Verify JACK is actually accepting connections AND matches
# our device (ignore rogue jackd -T from bt-a2dp's alsa_in)
if _jack_is_operational() and _jack_matches_config(self.config.output_device):
logger.info("JACK already running and operational — reusing")
return True
# Process exists but can't connect — likely a zombie that
@@ -356,7 +357,7 @@ class AudioSystem:
# Stabilization check — JACK can start briefly then crash
# if the ALSA device is flaky. Wait 1.5s with retries.
time.sleep(0.5)
if _jack_is_operational():
if _jack_is_operational() and _jack_matches_config(self.config.output_device):
# Auto-connect ports if enabled
if self.config.auto_connect:
self.connect_fx_ports()
@@ -388,6 +389,16 @@ class AudioSystem:
)
except Exception:
pass
# Kill alsa_in — bt-a2dp's bridge uses it, and when JACK dies
# alsa_in auto-spawns a rogue jackd -T -ndefault that satisfies
# _jack_is_operational() but connects as the wrong server.
try:
subprocess.run(
["killall", "alsa_in"],
capture_output=True, timeout=3,
)
except Exception:
pass
try:
subprocess.run(
["killall", "jackd"],
@@ -879,6 +890,35 @@ def _jack_is_operational() -> bool:
return False
def _jack_matches_config(cmdline_match: str = "") -> bool:
"""Check if the running jackd command matches expected args.
On systems with bt-a2dp-jack, ``alsa_in`` can auto-spawn a rogue
``jackd -T -ndefault`` that satisfies ``_jack_is_operational()``
but uses the wrong server name/ALSA device. Only return True if
the running jackd's ``/proc/*/cmdline`` contains the expected
device string.
Args:
cmdline_match: String to match in the jackd cmdline
(e.g. ``hw:USB,0`` or a period value).
"""
if not cmdline_match:
return True # no filter — just return operational status
try:
import glob as _glob
for proc_dir in _glob.glob("/proc/[0-9]*"):
try:
cmd = (Path(proc_dir) / "cmdline").read_bytes().decode("utf-8", errors="replace").replace("\0", " ")
if "jackd" in cmd and cmdline_match in cmd:
return True
except (OSError, PermissionError):
continue
return False
except Exception:
return True # on error, don't block — rely on operational check
# ═══════════════════════════════════════════════════════════════════
# JACK audio client (real-time I/O with pipeline)
# ═══════════════════════════════════════════════════════════════════