From 96bfe2e1505b4ae250f2d5201f9541ceb01cec84 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Thu, 29 Oct 2026 19:58:05 -0700 Subject: ts web slopify code --- run.py | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 run.py (limited to 'run.py') diff --git a/run.py b/run.py new file mode 100644 index 0000000..0ba7220 --- /dev/null +++ b/run.py @@ -0,0 +1,121 @@ +"""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() -- cgit v1.2.3