"""Start the dashboard and the browser voice gateway with one command. uv run run.py With web voice enabled (config.toml [voice] enabled = true) the Node gateway listens on the public port (web.port), handles /ws/voice and forwards every other request to Flask on 127.0.0.1:web.dashboard_port. With voice disabled (or Node missing) only the dashboard runs, directly on the public port. """ import os import shutil import signal import subprocess import sys import time from config import BASE_DIR, CONFIG GATEWAY_DIR = BASE_DIR / "gateway" MIN_NODE = (22, 12) def log(message): print(f"[run] {message}", flush=True) def node_ok(node): try: out = subprocess.run([node, "--version"], capture_output=True, text=True, check=True).stdout major, minor = (int(part) for part in out.strip().lstrip("v").split(".")[:2]) return (major, minor) >= MIN_NODE, out.strip() except (OSError, ValueError, subprocess.CalledProcessError): return False, "unknown" def ensure_gateway_deps(): if (GATEWAY_DIR / "node_modules" / "@honeybbq" / "teamspeak").is_dir(): return True npm = shutil.which("npm") if not npm: log("npm not found; cannot install the voice gateway dependencies") return False lock = (GATEWAY_DIR / "package-lock.json").exists() cmd = [npm, "ci" if lock else "install", "--omit=dev", "--no-audit", "--no-fund"] log(f"installing voice gateway dependencies: {' '.join(cmd[1:])}") return subprocess.run(cmd, cwd=GATEWAY_DIR).returncode == 0 def gateway_command(): """The Node command for the gateway, or None when voice cannot run.""" if not CONFIG["voice"].get("enabled"): log("web voice disabled in config.toml; starting the dashboard only") return None node = shutil.which("node") if not node: log("Node.js not found; starting the dashboard only (install Node >= 22.12 for web voice)") return None ok, version = node_ok(node) if not ok: log(f"Node.js {version} is too old (need >= {'.'.join(map(str, MIN_NODE))}); starting the dashboard only") return None if not ensure_gateway_deps(): log("voice gateway dependencies missing; starting the dashboard only") return None return [node, str(GATEWAY_DIR / "src" / "index.js")] def main(): web = CONFIG["web"] gateway_cmd = gateway_command() env = dict(os.environ) if gateway_cmd: env.update(DASHBOARD_HOST="127.0.0.1", DASHBOARD_LISTEN_PORT=str(web["dashboard_port"])) else: env.update(DASHBOARD_HOST=str(web["host"]), DASHBOARD_LISTEN_PORT=str(web["port"])) procs = [("dashboard", subprocess.Popen([sys.executable, str(BASE_DIR / "app.py")], cwd=BASE_DIR, env=env))] if gateway_cmd: procs.append(("gateway", subprocess.Popen(gateway_cmd, cwd=BASE_DIR, env=env))) log(f"open http://{'localhost' if web['host'] in ('0.0.0.0', '::') else web['host']}:{web['port']}") stopping = False def stop(*_): nonlocal stopping if stopping: return stopping = True for _, proc in procs: if proc.poll() is None: proc.send_signal(signal.SIGTERM) signal.signal(signal.SIGINT, stop) signal.signal(signal.SIGTERM, stop) exit_code = 0 try: while not stopping: for name, proc in procs: code = proc.poll() if code is not None: log(f"{name} exited with code {code}; shutting everything down") exit_code = code or 1 stop() break time.sleep(0.5) finally: stop() deadline = time.time() + 5 for _, proc in procs: try: proc.wait(timeout=max(0.1, deadline - time.time())) except subprocess.TimeoutExpired: proc.kill() sys.exit(exit_code) if __name__ == "__main__": main()