# -*- coding: utf-8 -*- """中继站:管理员改码 + 关机超 TTL 后不能丢(2.3.0)。 要钉死的行为: ① 管理员在后台把配对码从 111111 改成 222222 ② 那台电脑关机(或断网)超过 DEVICE_TTL(真实 6 小时) ③ snapshot() 把设备记录和"孤儿"码绑定都清掉 ④ 电脑开机,报**自己本地的旧码** 111111 ⑤ 中转应该回它 222222,并让它写回本地设置 —— 改动之前这里会收下 111111,管理员的改码白做 另外顺带验证: * 老格式 relay_state.json(knownDevices 是纯 ID 列表)能平滑升级 * 「删除设备」之后历史被忘干净(开关语义不能被绕) * 「抹码」之后历史里记的码也清空(否则抹码白做) """ from __future__ import annotations import json import os import subprocess import sys import tempfile import time import urllib.error import urllib.request from pathlib import Path ROOT = Path(__file__).resolve().parent.parent PORT = 8151 BASE = f"http://127.0.0.1:{PORT}" _UA = {"User-Agent": "LanRemote/2.3.0"} ADMIN_KEY = "recode-ttl-key" TTL = 2.0 passed = 0 failed = 0 def check(name: str, ok: bool, detail: str = "") -> bool: global passed, failed if ok: passed += 1 print(" [PASS] " + name) else: failed += 1 print(" [FAIL] " + name + ((" —— " + str(detail)) if detail else "")) return ok def main() -> int: work = Path(tempfile.mkdtemp(prefix="recode_ttl_")) data = work / "d" data.mkdir(parents=True, exist_ok=True) env = dict(os.environ) env.update({ "RELAY_PORT": str(PORT), "RELAY_BIND": "127.0.0.1", "RELAY_DATA": str(data), "RELAY_DEVICE_TTL": str(TTL), "RELAY_ADMIN_KEY": ADMIN_KEY, "RELAY_POLL": "2", "PYTHONIOENCODING": "utf-8", "PYTHONUNBUFFERED": "1", }) relay = None def start() -> subprocess.Popen: p = subprocess.Popen( [sys.executable, "-u", str(ROOT / "relay" / "relay.py")], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) for _ in range(80): time.sleep(0.4) try: health() return p except Exception: # noqa: BLE001 continue return p def health() -> dict: r = urllib.request.Request(BASE + "/v1/health", headers=_UA) with urllib.request.urlopen(r, timeout=5) as resp: return json.loads(resp.read().decode("utf-8")) def post(path: str, payload: dict, admin: bool = False) -> dict: body = json.dumps(payload).encode() headers = {**_UA, "Content-Type": "application/json"} if admin: headers["X-Admin"] = ADMIN_KEY req = urllib.request.Request(BASE + path, data=body, method="POST", headers=headers) try: with urllib.request.urlopen(req, timeout=20) as resp: return json.loads(resp.read().decode("utf-8")) except urllib.error.HTTPError as exc: try: return json.loads(exc.read().decode("utf-8")) except Exception: # noqa: BLE001 return {} def reg(dev: str, code: str, name: str = "教室电脑") -> dict: return post("/v1/agent/register", {"deviceId": dev, "code": code, "name": name, "version": "2.3.0"}) try: relay = start() dev = "classroom-PC-AAAA1111" old_code = "111111" new_code = "222222" print() print("═══ 1. 登记 + 管理员改码 ═══") r = reg(dev, old_code) check("① 首次登记成功", r.get("ok") is True, r) check("② 登记返回的码是它上报的", r.get("code") == old_code, r) res = post("/v1/admin/set_code", {"deviceId": dev, "code": new_code}, admin=True) check("③ 管理员改码成功", res.get("ok") is True, res) r2 = reg(dev, old_code) check("④ 在线时改码生效(回的是新码)", r2.get("ok") is True and r2.get("code") == new_code, r2) print() print(f"═══ 2. 电脑关机,等 {TTL + 2:.0f} 秒超过 TTL ═══") time.sleep(TTL + 2) h = health() check("⑤ 设备记录被清掉(设计中如此)", dev not in [d["deviceId"] for d in h["devices"]]) print(f" 清理后 codeCount={h.get('codeCount')}") print() print("═══ 3. 电脑开机,报本地旧码 —— 应该被纠正成新码 ═══") r3 = reg(dev, old_code) check("⑥ 开机登记成功", r3.get("ok") is True, r3) check("⑦ **写回的是新码 222222,不是它报的旧码**", r3.get("code") == new_code, f"拿到 {r3.get('code')!r},期望 {new_code!r}") check("⑧ 有 note 说明改码原因", bool(r3.get("note")), r3.get("note")) h2 = health() codes_now = [d.get("code") for d in h2["devices"]] check("⑨ /v1/health 里这台设备显示的也是新码", new_code in codes_now, codes_now) print() print("═══ 4. 重启中转 —— 新码要能持久化 ═══") relay.terminate() relay.wait(timeout=15) relay = start() r4 = reg(dev, old_code) check("⑩ 重启后仍然纠正成新码", r4.get("ok") is True and r4.get("code") == new_code, r4) print() print("═══ 5. 「删除设备」要忘干净(否���开关被绕过)═══") post("/v1/admin/delete", {"deviceId": dev}, admin=True) post("/v1/admin/allow_new", {"allow": False}, admin=True) r5 = reg(dev, new_code) check("⑪ 删除后设备被当成新设备,且开关关着 -> 拒绝", r5.get("ok") is False and r5.get("blocked") is True, r5) post("/v1/admin/allow_new", {"allow": True}, admin=True) print() print("═══ 6. 「抹码」之后历史里的码也要清空 ═══") dev2 = "classroom-PC-BBBB2222" reg(dev2, "333333") post("/v1/admin/forget", {"code": "333333"}, admin=True) r6 = reg(dev2, "444444") check("⑫ 抹码后可以换新码(历史没把旧码塞回来)", r6.get("ok") is True and r6.get("code") == "444444", r6) r7 = reg(dev2, "555555") check("⑬ 但换过之后又锁定单码制(回 444444)", r7.get("ok") is True and r7.get("code") == "444444", r7) print() print("=" * 62) print(f" 通过 {passed} 项,失败 {failed} 项") print("=" * 62) return 0 if failed == 0 else 1 finally: if relay is not None and relay.poll() is None: relay.terminate() try: relay.wait(timeout=10) except Exception: # noqa: BLE001 relay.kill() if __name__ == "__main__": sys.exit(main())