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()