文件
phonerail/rack/backend/prrack/main.py
T
zhou yang和Claude Opus 5.5 1cd4fd4082 rack:固件下载目录自动清理(每小时,FwStore.prune)
- 固件库里登记过的(界面上传)不自动删;没登记的(ota_push.py 直传、台架镜像)放满 7 天删;
  上传中断的 .tmp 1 小时删;非 <16 hex>.bin / .tmp 的文件名一律不碰
- 板子下载一次几秒就完、回滚用的是板上另一个槽,删旧文件不影响任何板子
- main.py:有 --fw-dir 时起清理循环,出错只记日志;test_firmware.py 加 2 例,共 41 例(本机 + 服务器)
- 已部署;本机模拟板实测放一个 8 天前的孤儿镜像,启动即被清掉

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 14:48:12 +08:00

135 行
4.9 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
"""入口。
生产(服务器上,systemd 起):
python -m prrack.main --db /root/phonerail-rack/data/rack.db \\
--mqtt-env /root/phonerail/mqtt.env --static /root/phonerail-rack/frontend/dist \\
--fw-dir /root/phonerail/fw
本机开发(不连 broker,起 N 块模拟板子,每秒一条遥测):
python -m prrack.main --sim 2 --db dev.db --static ../frontend/dist
⛔ 只许绑回环地址:api.py 按 L2 信任门户注入的 X-Auth-User,这个信任只在「外面够不着本进程」时成立。
"""
import argparse
import asyncio
import ipaddress
import logging
import os
import sys
import uvicorn
from .api import create_app
from .bus import MemBroker, MemBus, PahoBus
from .db import Db, now_ms
from .firmware import FwStore
from .hub import Hub
from .recovery import RecoveryManager
from .simboard import HUNG, SimBoard
log = logging.getLogger("prrack")
FW_GC_S = 3600 # 固件目录多久清一次(firmware.FwStore.prune)
def read_env(path: str) -> dict:
out = {}
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1)
out[k.strip()] = v.strip().strip('"')
return out
def build(args):
db = Db(args.db)
sims = []
if args.sim:
broker = MemBroker()
bus = MemBus(broker)
for i in range(args.sim):
present = (True, True, True, False, True) if i == 0 else (True,) * 5
sb = SimBoard(MemBus(broker), "5157ab%06x" % i, present=present, seed=i + 1)
if i == 0:
sb.set_phone(2, HUNG) # 留一台「卡死」的,给自动恢复看
sims.append(sb)
else:
env = read_env(args.mqtt_env)
bus = PahoBus(args.mqtt_host, args.mqtt_port, env.get("BACKEND_USER", "phonerail-backend"),
env["BACKEND_PASS"], "phonerail-rack-%d" % os.getpid())
hub = Hub(db, bus)
rec = RecoveryManager(hub, now_ms)
async def up():
await hub.start()
for sb in sims:
sb.start()
if sims:
async def ticker():
while True:
for sb in sims:
sb.tick(now_ms())
await asyncio.sleep(1)
asyncio.ensure_future(ticker())
async def down():
await hub.stop()
db.close()
static = args.static if args.static and os.path.isdir(args.static) else None
fw = FwStore(db, args.fw_dir, args.fw_base) if args.fw_dir else None
startup = [up]
if fw is not None:
async def fw_gc():
async def loop():
while True:
try:
gone = fw.prune()
if gone:
log.info("固件目录清理:删了 %d 个(%s)", len(gone), " ".join(gone))
except Exception: # 清理出错不能拖垮后端
log.exception("固件目录清理失败")
await asyncio.sleep(FW_GC_S)
asyncio.ensure_future(loop())
startup.append(fw_gc)
app = create_app(hub, rec, static, startup=startup, shutdown=[down], fw=fw)
return app
def ws_library_ok() -> bool:
for mod in ("websockets", "wsproto"):
try:
__import__(mod)
return True
except ImportError:
pass
return False
def main(argv=None):
ap = argparse.ArgumentParser(description="PhoneRail 机架管理后端")
ap.add_argument("--db", default="rack.db")
ap.add_argument("--static", default=None)
ap.add_argument("--bind", default="127.0.0.1")
ap.add_argument("--port", type=int, default=7764)
ap.add_argument("--mqtt-host", default="127.0.0.1")
ap.add_argument("--mqtt-port", type=int, default=1884)
ap.add_argument("--mqtt-env", default="/root/phonerail/mqtt.env")
ap.add_argument("--fw-dir", default=None,
help="固件下载目录(nginx 7765 的 /fw/,生产是 /root/phonerail/fw);不给就没有 OTA")
ap.add_argument("--fw-base", default="http://home.example:7765/fw",
help="板子下载固件用的 URL 前缀(板子只能走公网)")
ap.add_argument("--sim", type=int, default=0, help="不连 broker,起 N 块模拟板子")
args = ap.parse_args(argv)
if not ipaddress.ip_address(args.bind).is_loopback:
sys.exit("拒绝绑 %s:只许回环地址(见文件头)" % args.bind)
if not ws_library_ok():
sys.exit("没装 websockets / wsproto:uvicorn 会把 /api/ws 当普通 GET,界面能开但永远不刷新")
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
uvicorn.run(build(args), host=args.bind, port=args.port, log_level="info")
if __name__ == "__main__":
main()