fix: multiple issues preventing NAM from surviving rate/period changes
CI / test (push) Has been cancelled

Root causes of 'changing bit rate / sample rate kills NAM':

1. Zombie jackd from stale second Python process — when stop_jack()
   kills jackd, the other process's auto-restart respawns jackd with
   OLD config between stop and start. start_jack() sees a running
   jackd and returns True immediately without verifying it's actually
   accepting connections.

2. Zombie Python process from incomplete service restart — two
   main.py instances running, each with its own AudioSystem and
   JackAudioClient. Profile change kills one JACK but the other
   respawns it. jack_client.start() then fails with server_error
   because the stale SHM conflicts.

3. start_jack() returned True for a dead jackd — process exists but
   JackServer::Open failed with -1 (bad period/rate for device).
   No SHM socket was created, but the process check passed.

Fixes:
- _ensure_singleton(): kill any other main.py process at boot
- start_jack(): verify jack_default_0_0 SHM socket exists before
  returning True; kill zombie process if socket is missing
- Profile handler: clean SHM right before jack_client.start()
- JackAudioClient.start(): retry once with SHM cleanup on
  server_error (0x21)
This commit is contained in:
2026-06-18 17:21:04 -04:00
parent c0c2538c80
commit f3cdf5e13b
2 changed files with 41 additions and 3 deletions
+25
View File
@@ -55,6 +55,28 @@ from src.system.config import DEFAULT_CONFIG_PATH, load_config, save_config, DEF
_AUTH_PIN_SEEN = False
def _ensure_singleton() -> None:
"""Kill any other main.py processes (from stale service restarts)."""
import subprocess, os
my_pid = os.getpid()
try:
r = subprocess.run(
["pgrep", "-f", "python3.*main.py"],
capture_output=True, text=True, timeout=3,
)
if r.returncode == 0:
for pid_str in r.stdout.strip().split():
pid = int(pid_str)
if pid != my_pid:
try:
os.kill(pid, 9)
logger.info("Killed stale pedal process PID %d", pid)
except (OSError, ProcessLookupError):
pass
except Exception:
pass
def _ensure_auth_pin(config: dict, config_path: Path) -> str:
"""Generate a random 6-digit auth PIN on first boot and persist it to config.
@@ -790,6 +812,9 @@ def main() -> int:
Returns exit code 0 on clean shutdown, 1 on boot failure.
"""
# Kill any stale pedal processes before doing anything else
_ensure_singleton()
# Lock process memory early to prevent page faults in RT callback
try:
import ctypes
+16 -3
View File
@@ -288,10 +288,23 @@ class AudioSystem:
logger.info("JACK disabled in config")
return False
# Already running?
# Already running? Check that the running JACK matches our
# desired config — a zombie jackd from another process can
# satisfy _jack_is_running() but have wrong settings.
if _jack_is_running():
logger.info("JACK already running")
return True
# Verify via /dev/shm/jack_default_0_0 socket that the
# running JACK is actually accepting connections.
if Path("/dev/shm/jack_default_0_0").is_socket():
logger.info("JACK already running and operational — reusing")
return True
# Process exists but no socket — likely a zombie that
# spawned but failed to init audio. Kill it first.
logger.warning("JACK process exists but not operational — killing")
try:
subprocess.run(["killall", "-9", "jackd"], capture_output=True, timeout=3)
time.sleep(0.5)
except Exception:
pass
profile = self.config.latency_profile
jack_env = os.environ.copy()