Files
ykxmz/_build/fix_session_steal.py
xmz7891 b62ac865ea ykxmz 1.5.0:修复手表端 TLS 证书校验 + MTU 修复网络稳定性
被控端
- 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%)
- 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败)
- 每分钟回头找局域网,自动切回快 400 倍的路径
- 画面心跳帧:静止时省流量,客户端仍知道流是活的
- 锁屏时给出可操作的提示

手表端
- 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException
- 连接错误翻译成人话(证书/时间/解析/超时分别提示)
- 经中转连上后用服务器报的地址刷新已配对记录
- 新增局域网连接页面、服务器延迟检测

服务器端
- 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502)
- 网络调优脚本 net-tune.sh(开机自动应用)
- 管理页:批量删除、网络体检、下载区
- 隧道改 HTTP/2 + 双连接

文档
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
2026-10-06 19:01:05 +08:00

217 lines
8.7 KiB
Python
Raw Permalink 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 -*-
"""修:被顶替的会话抢走了本该给新会话的数据。
证据(Java 客户端打点):
[dbg] attach ok session=ad9283e7 <- 新手表接入
[dbg] send 122 bytes -> 200 <- 新手表发 auth
[dbg] poll -> 200 bytes=53 session=ad9283e7 <- 新手表其实收到了
[dbg] poll -> 200 bytes=50 session=a7c11203 <- 但旧手表也收到了一份
根因:
device.to_client 是**按设备共享**的一个缓冲区。
旧会话被顶掉后,它那个 40 秒的长轮询还挂在 device.event 上。
新数据一来,谁先醒谁拿走 —— 旧会话就把它截胡了。
修法:
1. Session 各自带一个 event
2. 顶替旧会话时,把它的 event set() 一下 —— 它的长轮询立刻醒来
3. client_take 每轮先看 session.replaced,是就抛 _Replaced
4. 接口层把 _Replaced 翻成 409 + replaced,客户端据此断开(不抢)
5. 数据只唤醒**当前**会话(device.active_session)
"""
from __future__ import annotations
import sys
from pathlib import Path
RELAY = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\relay\relay.py")
# ---------------- 1. Device 记住当前会话对象 ----------------
OLD_SLOTS = ''' "event", "bytes_in", "bytes_out", "current_session")'''
NEW_SLOTS = ''' "event", "bytes_in", "bytes_out", "current_session",
"active_session")'''
OLD_INIT = ''' self.current_session = ""'''
NEW_INIT = ''' self.current_session = ""
# 当前挂在这台设备上的 Session 对象。数据来了只唤醒它,
# 不要让已经被顶掉的旧会话把数据截胡。
self.active_session = None'''
# ---------------- 2. push_to_client 只唤醒当前会话 ----------------
OLD_PUSH = ''' def push_to_client(self, data: bytes) -> None:
if not data:
return
self.bytes_out += len(data)
self._append(self.to_client, data)
self.event.set()'''
NEW_PUSH = ''' def push_to_client(self, data: bytes) -> None:
if not data:
return
self.bytes_out += len(data)
self._append(self.to_client, data)
self.event.set()
# 同时叫醒当前那个会话的长轮询(旧会话已经被替换,不该再拿数据)
session = self.active_session
if session is not None:
session.event.set()'''
# ---------------- 3. Session 带自己的 event + replaced ----------------
OLD_SESSION = ''' __slots__ = ("token", "device_id", "code", "name", "ip",
"created", "last_seen")
def __init__(self, device_id: str, code: str, name: str, ip: str) -> None:
self.token = secrets.token_hex(24)
self.device_id = device_id
self.code = code
self.name = name
self.ip = ip
self.created = time.time()
self.last_seen = time.time()'''
NEW_SESSION = ''' __slots__ = ("token", "device_id", "code", "name", "ip",
"created", "last_seen", "event", "replaced")
def __init__(self, device_id: str, code: str, name: str, ip: str) -> None:
self.token = secrets.token_hex(24)
self.device_id = device_id
self.code = code
self.name = name
self.ip = ip
self.created = time.time()
self.last_seen = time.time()
# 自己的唤醒事件。每个会话独立等待,避免被顶掉的旧会话截胡数据。
self.event = threading.Event()
self.replaced = False'''
# ---------------- 4. attach:踢的时候唤醒旧会话 ----------------
OLD_KICK = ''' if stale.device_id == device_id:
self.sessions.pop(stale_token, None)
self.retired[stale_token] = "replaced"
log(f"踢掉旧会话 {stale_token[:8]}…(同一设备只允许一个)")'''
NEW_KICK = ''' if stale.device_id == device_id:
self.sessions.pop(stale_token, None)
self.retired[stale_token] = "replaced"
stale.replaced = True
# 立刻叫醒它挂着的长轮询 —— 否则它会一直等下去,
# 等新数据来时把本该给新手表的数据抢走。
stale.event.set()
log(f"踢掉旧会话 {stale_token[:8]}…(同一设备只允许一个)")'''
OLD_BIND = ''' device.current_session = session.token
# 换会话了:把上一条留在队列里的半截数据清掉,
# 不然新手表会收到上一个手表没取走的残留字节。
device.to_client.clear()'''
NEW_BIND = ''' device.current_session = session.token
device.active_session = session
# 换会话了:把上一条留在队列里的半截数据清掉,
# 不然新手表会收到上一个手表没取走的残留字节。
device.to_client.clear()'''
# ---------------- 5. client_take:独立事件 + 让位 ----------------
OLD_TAKE = ''' def client_take(self, session: Session, wait: float) -> bytes:
"""手表取走「被控端发来的」数据。"""
with self.lock:
device = self.devices.get(session.device_id)
if device is None:
return b""
return self._take(device, device.to_client, wait, "client")'''
NEW_TAKE = ''' def client_take(self, session: Session, wait: float) -> bytes:
"""手表取走「被控端发来的」数据。
注意这里**不能**复用 _take:那个是按设备的事件等的,
会把被顶掉的旧会话和新会话混在一起等,导致数据被旧会话截胡。
每个会话等自己的 event,并且每轮先看自己是不是已经被顶替了。
"""
deadline = time.time() + wait
while True:
if session.replaced:
raise SessionReplaced()
with self.lock:
device = self.devices.get(session.device_id)
if device is None:
return b""
if device.to_client:
data = bytes(device.to_client)
device.to_client.clear()
device.event.clear()
session.event.clear()
return data
remaining = deadline - time.time()
if remaining <= 0:
return b""
session.event.wait(min(remaining, 0.5))
session.event.clear()'''
# ---------------- 6. _Replaced 异常 ----------------
OLD_EXC_ANCHOR = '''class Relay:'''
NEW_EXC = '''class SessionReplaced(Exception):
"""这个会话已经被新的手表顶替了 —— 它必须让位,不能继续取数据。"""
class Relay:'''
# ---------------- 7. 接口层翻成 409 ----------------
OLD_POLL = ''' data = RELAY.client_take(session, POLL_SECONDS)
self._send(200, data, "application/octet-stream")
return'''
NEW_POLL = ''' try:
data = RELAY.client_take(session, POLL_SECONDS)
except SessionReplaced:
self._json(409, {"ok": False, "replaced": True,
"error": "已被另一台手表接管"})
return
self._send(200, data, "application/octet-stream")
return'''
# ---------------- 8. push_to_agent 也唤醒对应会话(agent 侧不用改) ----------------
def main() -> int:
text = RELAY.read_text(encoding="utf-8")
ok = True
for old, new, tag in ((OLD_SLOTS, NEW_SLOTS, "Device 加 active_session"),
(OLD_INIT, NEW_INIT, "初始化 active_session"),
(OLD_PUSH, NEW_PUSH, "push 只唤醒当前会话"),
(OLD_SESSION, NEW_SESSION, "Session 带 event/replaced"),
(OLD_KICK, NEW_KICK, "踢的时候唤醒旧会话"),
(OLD_BIND, NEW_BIND, "绑定 active_session"),
(OLD_TAKE, NEW_TAKE, "client_take 独立等待 + 让位"),
(OLD_EXC_ANCHOR, NEW_EXC, "SessionReplaced 异常"),
(OLD_POLL, NEW_POLL, "poll 把让位翻成 409")):
if old not in text:
print(f" [失败] {tag} —— 锚点没找到")
ok = False
continue
text = text.replace(old, new, 1)
print(f" {tag}")
RELAY.write_text(text, encoding="utf-8")
import ast
try:
ast.parse(text)
print(" 语法 OK")
except SyntaxError as exc:
print(f" 语法错误: {exc}")
ok = False
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())