From f3cdf5e13bbf347d0e83fe5833319f94a30e433e Mon Sep 17 00:00:00 2001 From: Shawn Date: Thu, 18 Jun 2026 17:21:04 -0400 Subject: [PATCH] fix: multiple issues preventing NAM from surviving rate/period changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- main.py | 25 +++++++++++++++++++++++++ src/system/audio.py | 19 ++++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 1736d88..4990488 100644 --- a/main.py +++ b/main.py @@ -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 diff --git a/src/system/audio.py b/src/system/audio.py index cbc3872..af4c39e 100644 --- a/src/system/audio.py +++ b/src/system/audio.py @@ -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()