aboutsummaryrefslogtreecommitdiffstats
path: root/run.py
blob: 0ba7220b3534bcd6872a1d6770ce67afaa4687a1 (plain) (blame)
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()
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage