Files
ykxmz/_tests/test_recode_survives_ttl.py
T
xmz7891 2061f9525f 2.3.0:手表界面精简 + 播放器 + 鼠标重做
手表端
- 功能页从 10 个杂项精简到 7 个(屏幕/鼠标/媒体/文件/电脑/语音/电源)
- 删掉:游戏封锁、待办、掷骰子、抽奖、壁纸、显示器开关、定时关机、
  剪贴板历史、性能/磁盘/电量、文件夹浏览、教室限制等
- 改成显式列按钮,不再把电脑端所有功能组无脑铺到表盘
- 远程鼠标控制重做:滑动区 + 左键/滚轮/右键三按钮
  滚轮点按=中键、上下滑=滚动
- 新增视频/音乐播放器(手表本地播放):进度拖动、左右滑快进退10秒、
  点画面播放暂停、上一/下一、播完自动下一首

电脑端
- 新增 kill_all(结束所有进程),白名单保护系统关键进程和自身
- 新增 media_list / media_pull(播放器数据源)
  安全边界:只允许媒体目录,用 relative_to 判断,防同前缀兄弟目录绕过

中转站
- known_devices 从 ID 集合改成「设备ID -> 配对码」映射
- 修复:管理员改码后关机超过 6 小时,改码会被超时清理冲掉
- 老格式 state 文件平滑升级
2026-10-18 11:39:14 +08:00

193 lines
7.0 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.
# -*- 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())