# -*- coding: utf-8 -*- r'''修 3(最严重):TTL 清理 + 关掉接入开关 = 设备永久锁死。 == 问题 == register() 里判断"是不是老设备"用的是: known_device = device_id in self.devices 但 snapshot() 会把超过 DEVICE_TTL(6 小时)没上报的设备 从 devices **和 codes 里一起删掉**。 于是这条链必然发生: 1. 电脑关机过夜(> 6 小时) -> 设备被清出 devices 2. 管理员关着"允许新设备接入" -> 第二天开机的电脑被当新设备拒绝 3. **重启中转也救不回来**: snapshot 清 codes 时没调 _save(),磁盘上绑定还在 -> 重启 _load() 恢复绑定 -> 第一次 snapshot() 又当孤儿清掉 这是个自我维系的死循环 4. 管理员在页面上**看不到这台设备了**,不知道该放开谁 == 修法 == 状态文件里单独存一份 `knownDevices`(设备历史名单)。 **超时清理只动 devices / codes,绝不动 knownDevices。** "是不是老设备"改成查这份历史名单。 同时: * snapshot 清孤儿 codes 之后补 _save(),让磁盘状态和内存一致 (否则又是一处"重启后状态回滚"的隐患) * delete_device / forget 才是"真正忘记这台设备"的地方, 那里要把 knownDevices 一起删掉 —— 否则管理员"删除设备"之后 那台设备仍然算老设备,开关就形同虚设 ''' from __future__ import annotations import sys from pathlib import Path ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") SRC = ROOT / "relay" / "relay.py" def patch(text, old, new, label): if old not in text: print(" ✗ 没找到:%s" % label) return text, False if text.count(old) != 1: print(" ! 出现 %d 次:%s" % (text.count(old), label)) return text, False print(" ✓ %s" % label) return text.replace(old, new, 1), True def main() -> int: text = SRC.read_text(encoding="utf-8") ok_all = True # ---------------------------------------------------------- 字段 text, ok = patch( text, """ # 是否允许新设备接入。False 时只有已登记过的 deviceId 能上线。 self.allow_new = True""", """ # 是否允许新设备接入。False 时只有已登记过的 deviceId 能上线。 self.allow_new = True # 设备历史名单(只有 deviceId,不含配对码)。 # **超时清理绝不动它** —— 它是"这台设备曾经被允许过"的唯一凭证。 # 详见 register() 里 known_device 的注释。 self.known_devices: set = set()""", "加 known_devices 字段") ok_all = ok_all and ok # ---------------------------------------------------------- _load text, ok = patch( text, """ # 是否允许新设备接入(重启不丢) self.allow_new = bool(raw.get("allowNew", True))""", """ # 是否允许新设备接入(重启不丢) self.allow_new = bool(raw.get("allowNew", True)) # 恢复设备历史名单(重启不丢) for did in raw.get("knownDevices", []): did = str(did or "").strip() if did: self.known_devices.add(did)""", "_load 恢复 known_devices") ok_all = ok_all and ok # ---------------------------------------------------------- _save text, ok = patch( text, """ payload = { "allowNew": bool(self.allow_new), "codes": [""", """ payload = { "allowNew": bool(self.allow_new), # 设备历史:超时清理不会删它,所以关掉接入开关后 # 老设备第二天开机仍然能上线 "knownDevices": sorted(self.known_devices), "codes": [""", "_save 持久化 known_devices") ok_all = ok_all and ok # ---------------------------------------------------------- register text, ok = patch( text, """ known_device = device_id in self.devices if not known_device and not self.allow_new:""", """ # ⚠ 判断"老设备"必须查 known_devices(设备历史), # **不能查 self.devices** —— devices 里超过 6 小时没上报的 # 会被 snapshot() 清掉(电脑关机过夜必然超时), # 那时老设备会被自己的开关拒之门外,而且管理员在页面上 # 已经看不到它、不知道该放开谁,重启也救不回来。 known_device = device_id in self.known_devices if not known_device and not self.allow_new:""", "register 改查 known_devices") ok_all = ok_all and ok # 登记成功时记进历史 text, ok = patch( text, """ # 每次登记都刷新局域网信息(IP 可能变,端口也可能改) device.lan_ips = lan_ips""", """ # 记进设备历史名单(这是"曾经被允许过"的凭证,只增不减; # 只有管理员显式删除设备时才会被移除) self.known_devices.add(device_id) # 每次登记都刷新局域网信息(IP 可能变,端口也可能改) device.lan_ips = lan_ips""", "register 登记成功时记入历史") ok_all = ok_all and ok # ---------------------------------------------------------- snapshot 补 _save text, ok = patch( text, """ orphan = [code for code, owner in self.codes.items() if owner not in self.devices] for code in orphan: self.codes.pop(code, None)""", """ orphan = [code for code, owner in self.codes.items() if owner not in self.devices] for code in orphan: self.codes.pop(code, None) # ⚠ 清完孤儿码要落盘。不然内存里没了、磁盘上还在, # 重启后 _load() 又恢复回来,下一次 snapshot() 再清一遍 —— # 看上去能用,但每次重启都会"回滚"一次状态,很难排查。 # (超时清理 devices 也有同样问题,一并落盘。) if stale or orphan: self._save()""", "snapshot 清理后落盘") ok_all = ok_all and ok # ---------------------------------------------------------- delete / forget text, ok = patch( text, """ self.devices.pop(target.device_id, None) # 把它的码绑定一起删掉 gone_codes = [c for c, owner in list(self.codes.items()) if owner == target.device_id]""", """ self.devices.pop(target.device_id, None) # "删除设备"是**真正忘记这台设备** —— 连历史名单一起删。 # 不删的话,管理员点了删除、开关又关着, # 那台设备下次来仍然被当成老设备放行,开关就形同虚设。 self.known_devices.discard(target.device_id) # 把它的码绑定一起删掉 gone_codes = [c for c, owner in list(self.codes.items()) if owner == target.device_id]""", "delete_device 连带清历史") ok_all = ok_all and ok text, ok = patch( text, """ def forget(self, code: str) -> bool: \"\"\"抹掉一个配对码绑定(让那台设备下次可以设新码)。\"\"\" with self.lock: device_id = self.codes.pop(code, None) if not device_id: return False""", """ def forget(self, code: str) -> bool: \"\"\"抹掉一个配对码绑定(让那台设备下次可以设新码)。 注意:**只抹码,不抹设备历史**。 抹码的语义是"这个码我不占了",不是"我不认识这台设备了"。 \"\"\" with self.lock: device_id = self.codes.pop(code, None) if not device_id: return False""", "forget 加注释说明不动历史") ok_all = ok_all and ok if not ok_all: print() print(" 有锚点没匹配,没有写回") return 1 SRC.write_text(text, encoding="utf-8", newline="\n") print() print(" 已写回 relay.py") # 复核 for token in ("known_devices", '"knownDevices"', "known_device = device_id in self.known_devices"): print(" %s %s" % ("✓" if token in text else "✗", token)) return 0 if __name__ == "__main__": sys.exit(main())