Files
MusicServer/scripts/serve.py
T
snowy-yang 0cae770ace feat(server): 支持 Granian 与 rloop/uvloop/winloop 事件循环
- 新增 scripts/serve.py 启动器:server auto/uvicorn/granian,循环可用性与平台校验(显式不可用直接报错)
- pyproject 增加 granian 可选 extra(granian[reload,rloop,uvloop,winloop],平台 marker 由 granian 维护)
- tests/test_serve.py 覆盖服务器/循环选择与命令拼装;README 补充启动方式、循环矩阵与多 worker 限制
2026-10-23 22:12:08 +08:00

172 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""启动器 —— uvicorn / granian,可选事件循环 rloop / uvloop / winloop。
用法(在仓库根目录执行):
uv run python scripts/serve.py # 默认:装了 granian 就优先 granian,否则 uvicorn
uv run python scripts/serve.py --server granian # Rust HTTP 服务器
uv run python scripts/serve.py --server granian --loop rloop # 仅 Unix(granian[rloop])
uv run python scripts/serve.py --server granian --loop uvloop # 仅 Unix(granian[uvloop])
uv run python scripts/serve.py --server granian --loop winloop # 仅 Windows(granian[winloop])
uv run python scripts/serve.py --server uvicorn --loop uvloop # uvicorn 在 Unix 上等价
事件循环可用性(与 granian 的 extras / 平台 marker 一致):
asyncio 始终可用
rloop rloop 包,仅 Unix(granian 内置的 Rust 循环)
uvloop uvloop 包,仅 Unix
winloop winloop 包,仅 Windows(uvloop 的 Windows 实现)
`auto` 交给服务器自行选择:granian 优先 rloop → uvloop → winloop → asyncio;
uvicorn 优先 uvloop(已安装时)→ asyncio。
环境变量(参数优先):SERVER / LOOP / HOST / PORT / WORKERS / RELOAD。
PORT 默认读取 .env 后的 16688;HOST 默认 0.0.0.0(局域网播放器可直连)。
"""
from __future__ import annotations
import argparse
import importlib.util
import os
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from app import __version__, config # noqa: E402
APP_TARGET = "app.main:app"
SERVERS = ("auto", "uvicorn", "granian")
LOOPS = ("auto", "asyncio", "rloop", "uvloop", "winloop")
#: 每个循环对应的可导入包(asyncio 无依赖)
LOOP_PACKAGES = {"rloop": "rloop", "uvloop": "uvloop", "winloop": "winloop"}
def has_module(name: str) -> bool:
"""模块是否可导入(不实际导入,避免触发事件循环初始化)。"""
try:
return importlib.util.find_spec(name) is not None
except (ImportError, ValueError):
return False
def pick_server(requested: str) -> str:
"""auto:安装了 granian 就用 granian,否则 uvicorn。"""
if requested not in SERVERS:
raise SystemExit(f"不支持的 server: {requested}(可选:{', '.join(SERVERS)})")
if requested != "auto":
return requested
return "granian" if has_module("granian") else "uvicorn"
def available_loops(server: str) -> list[str]:
"""当前平台/安装状态下该服务器可用的事件循环(含 auto)。"""
loops = ["auto", "asyncio"]
if server == "granian":
loops.extend(name for name in ("rloop", "uvloop", "winloop") if has_module(LOOP_PACKAGES[name]))
elif has_module("uvloop"):
loops.append("uvloop")
return loops
def resolve_loop(server: str, loop: str) -> str:
"""校验事件循环可用性;显式请求不可用时给出可操作的原因(不做静默降级)。"""
if loop not in LOOPS:
raise SystemExit(f"不支持的 loop: {loop}(可选:{', '.join(LOOPS)})")
if loop == "auto":
return loop
if server == "uvicorn":
if loop == "asyncio":
return loop
if loop == "uvloop":
if not has_module("uvloop"):
raise SystemExit("uvloop 未安装(仅 Unix 可用):uv sync --extra granian 或改用 --loop auto")
return loop
raise SystemExit(f"uvicorn 不支持 --loop {loop};请改用 --server granian(granian 支持 rloop/uvloop/winloop)")
package = LOOP_PACKAGES.get(loop)
if package and not has_module(package):
hint = "仅 Windows 可用" if loop == "winloop" else "仅 Unix 可用"
raise SystemExit(
f"granian 的 {loop} 事件循环不可用({package} 未安装,{hint})。可用:{', '.join(available_loops(server))}"
)
return loop
def _env_bool(name: str) -> bool:
return os.environ.get(name, "").strip().lower() in ("1", "true", "yes", "on")
def build_command(
server: str,
*,
host: str,
port: int,
workers: int,
loop: str,
reload: bool,
) -> list[str]:
command = [sys.executable, "-m", server]
if server == "granian":
command += ["--interface", "asgi", "--host", host, "--port", str(port)]
command += ["--workers", str(workers), "--loop", loop]
if reload:
command.append("--reload")
if reload and not has_module("watchfiles"):
print("[serve] 警告:granian --reload 需要 watchfiles(uv sync --extra granian 已包含)", flush=True)
command.append(APP_TARGET)
else:
command += [APP_TARGET, "--host", host, "--port", str(port)]
command += ["--workers", str(workers), "--loop", loop]
if reload:
command.append("--reload")
return command
def _arg_or_env(value: str | None, env_name: str, default: str | None = None) -> str | None:
return value if value is not None else os.environ.get(env_name, default)
def main(argv: list[str] | None = None) -> int:
config.load_env()
parser = argparse.ArgumentParser(description="MusicServer 启动器(uvicorn / granian)")
parser.add_argument("--server", choices=SERVERS, help="HTTP 服务器(默认 auto:优先 granian)")
parser.add_argument("--loop", choices=LOOPS, help="事件循环(默认 auto)")
parser.add_argument("--host", help="监听地址(默认 0.0.0.0)")
parser.add_argument("--port", type=int, help="端口(默认 PORT 或 16688)")
parser.add_argument("--workers", type=int, help="worker 进程数(默认 1)")
parser.add_argument("--reload", action="store_true", help="开发热重载(强制单 worker)")
args = parser.parse_args(argv)
server = pick_server(str(_arg_or_env(args.server, "SERVER", "auto")))
if not has_module(server):
raise SystemExit(f"未安装 {server}:uv sync --extra granian(或 uv add {server})")
loop = resolve_loop(server, str(_arg_or_env(args.loop, "LOOP", "auto")))
host = str(_arg_or_env(args.host, "HOST", "0.0.0.0"))
port = args.port or config.port()
workers = args.workers or int(os.environ.get("WORKERS") or 1)
reload = args.reload or _env_bool("RELOAD")
if reload and workers > 1:
print(f"[serve] --reload 与多 worker 不兼容,workers 从 {workers} 改为 1", flush=True)
workers = 1
command = build_command(server, host=host, port=port, workers=workers, loop=loop, reload=reload)
print(
f"[serve] MusicServer {__version__} | {server} | loop={loop} | http://{host}:{port} "
f"| workers={workers}{' | reload' if reload else ''}",
flush=True,
)
try:
return subprocess.call(command)
except KeyboardInterrupt: # Ctrl+C 由子进程自行处理,父进程安静退出
return 0
if __name__ == "__main__":
raise SystemExit(main())