This commit is contained in:
Your Name
2026-08-27 14:04:28 +08:00
parent f7720831be
commit 334890171e
3016 changed files with 263403 additions and 27971 deletions
+198
View File
@@ -0,0 +1,198 @@
# -*- coding: utf-8 -*-
"""一次拉起后端的三个服务。
服务是拆开的,但开发和小规模部署时逐个开三个终端太啰嗦。这个脚本把它们放在一个
进程组里:任何一个退出就把其余的一起停掉——留着半套服务在跑,比全停更难排查。
python run_backend.py # 三个都起
python run_backend.py --only api gateway # 只起指定的
python run_backend.py --db /data/backend.db
生产环境请用 systemd / NSSM 之类的进程守护,各服务独立托管、独立重启:
一个模型上游雪崩不应该把管理后台一起带走,这正是当初把它们拆开的原因。
"""
from __future__ import annotations
import argparse
import os
import signal
import socket
import subprocess
import sys
import threading
import time
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
SERVICES = {
"api": {
# 老的网页后台(console/8765)已整体退役:界面由 Vue 管理端承担,
# 桌面端同步、调用留痕上报也都并进了这个服务。
"label": "管理端(Vue 前端 + JSON API + 桌面端同步)",
"default_port": 8766,
"argv": lambda db, host, port: [
sys.executable, "admin_api.py",
"--db", str(db), "--host", host, "--port", str(port),
],
},
"gateway": {
"label": "模型网关(答题 + 裁判)",
"default_port": 8770,
"argv": lambda db, host, port: [
sys.executable, "model_gateway.py",
"--db", str(db), "--host", host, "--port", str(port),
],
},
}
def _port_busy(host: str, port: int) -> bool:
"""这个端口是不是已经被占了。
提前查一遍,是为了给一句人话。让 uvicorn 自己撞上去的话,Windows 会把
WinError 10048 的系统文案按本地代码页塞进管道,输出一串乱码——看到的人
只知道"起不来",不知道是端口冲突,更不知道是谁占着。
"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
probe.settimeout(0.4)
try:
return probe.connect_ex((host if host != "0.0.0.0" else "127.0.0.1", port)) == 0
except OSError:
return False
def _who_holds(port: int) -> str:
"""尽力查出占用端口的进程,查不到就算了——这只是帮忙,不是必需。"""
try:
output = subprocess.run(
["netstat", "-ano", "-p", "tcp"],
capture_output=True, text=True, timeout=5,
).stdout
except Exception:
return ""
for line in output.splitlines():
parts = line.split()
if len(parts) >= 5 and parts[3] == "LISTENING" and parts[1].endswith(f":{port}"):
pid = parts[4]
try:
name = subprocess.run(
["tasklist", "/FI", f"PID eq {pid}", "/FO", "CSV", "/NH"],
capture_output=True, text=True, timeout=5,
).stdout.split(",")[0].strip().strip(chr(34))
except Exception:
name = ""
return f"{name or '未知进程'}PID {pid}"
return ""
def _pump(name: str, stream) -> None:
"""把子进程的输出打上服务名前缀转发出来,三路日志混在一起才分得清。"""
for raw in iter(stream.readline, ""):
line = raw.rstrip()
if line:
print(f"[{name}] {line}", flush=True)
def main() -> None:
parser = argparse.ArgumentParser(description="拉起后端服务")
parser.add_argument("--db", default=str(SCRIPT_DIR / "backend.db"))
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument(
"--only", nargs="*", choices=sorted(SERVICES), default=sorted(SERVICES)
)
for name, spec in SERVICES.items():
parser.add_argument(f"--{name}-port", type=int, default=spec["default_port"])
args = parser.parse_args()
db = Path(args.db).resolve()
# 管理端负责建表和播种 admin,必须最先起来:网关和它读同一个库,表不在就只能报错
order = [name for name in ("api", "gateway") if name in args.only]
if not order:
raise SystemExit("没有选中任何服务")
# 先把端口全查一遍再启动。逐个起的话,第三个服务撞上占用端口时前两个已经
# 在跑了——退出流程会把它们一起停掉,你看到的是"全部服务已停止",得往回翻
# 好几屏才找得到真正的原因。
busy = [
(name, getattr(args, f"{name}_port"))
for name in order
if _port_busy(args.host, getattr(args, f"{name}_port"))
]
if busy:
print("以下端口已被占用,服务没有启动:")
print()
for name, port in busy:
holder = _who_holds(port)
tail = f" ← 被 {holder} 占用" if holder else ""
print(f" {name:8s} {args.host}:{port}{tail}")
first_name, first_port = busy[0]
print()
print("处理办法(任选其一):")
print(" 1. 关掉占用它的进程——多半是上一次没退干净的同一套服务")
print(
f" 2. 换端口启动:python run_backend.py "
f"--{first_name}-port {first_port + 10}"
)
print(" 查占用:netstat -ano | findstr :" + str(first_port))
raise SystemExit(1)
procs: dict[str, subprocess.Popen] = {}
print(f"数据库:{db}")
for name in order:
spec = SERVICES[name]
port = getattr(args, f"{name}_port")
proc = subprocess.Popen(
spec["argv"](db, args.host, port),
cwd=SCRIPT_DIR,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding="utf-8",
errors="replace",
bufsize=1,
)
procs[name] = proc
threading.Thread(
target=_pump, args=(name, proc.stdout), daemon=True
).start()
print(f"已启动 {name:8s} http://{args.host}:{port} {spec['label']}")
if name == "api":
# 让建表先跑完,网关起来时表就已经在了
time.sleep(1.5)
print("\nCtrl+C 停止全部服务。\n")
try:
while True:
for name, proc in procs.items():
code = proc.poll()
if code is not None:
print(f"\n[{name}] 已退出(code={code}),正在停止其余服务…")
raise KeyboardInterrupt
time.sleep(0.5)
except KeyboardInterrupt:
pass
finally:
for name, proc in procs.items():
if proc.poll() is None:
try:
if os.name == "nt":
proc.terminate()
else:
proc.send_signal(signal.SIGINT)
except OSError:
pass
deadline = time.time() + 8
for name, proc in procs.items():
remaining = max(0.5, deadline - time.time())
try:
proc.wait(timeout=remaining)
except subprocess.TimeoutExpired:
print(f"[{name}] 没有及时退出,强制结束")
proc.kill()
print("全部服务已停止。")
if __name__ == "__main__":
main()