1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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()
|