# -*- coding: utf-8 -*- r'''用**真实被控端**复现「经中转几次之后就不响应」。 中转本身的队列逻辑已经单独验过是对的(_build/repro_relay_hang.py 13/13 通过), 所以问题在 windows/lanremote/relay.py 的客户端侧,或者它和中转的交互。 这个脚本: 1. 配一个 Store(中转指向本地),起本地中转 2. 起真实 run_agent.py --silent 3. 等它登记 4. 接入手表会话 5. 依次发 pair / ping / ping / feature_list / info,每次都读回复 6. 把被控端的日志和解码后的数据流全部打出来 这样卡在哪一步、被控端收到了什么、回了什么,一目了然。 ''' from __future__ import annotations import json import os import shutil import subprocess import sys import tempfile import time import urllib.error import urllib.request from pathlib import Path ROOT = Path(__file__).resolve().parent.parent WINDOWS = ROOT / "windows" RELAY_PORT = 8124 AGENT_PORT = 8125 BASE = f"http://127.0.0.1:{RELAY_PORT}" PAIR = "246810" UA = {"User-Agent": "LanRemote/1.8.2"} def post(path, body=b"", headers=None, timeout=20.0): h = dict(UA) h.update(headers or {}) req = urllib.request.Request(BASE + path, data=body, headers=h, method="POST") try: with urllib.request.urlopen(req, timeout=timeout) as resp: return resp.status, resp.read() except urllib.error.HTTPError as exc: return exc.code, exc.read() except Exception as exc: # noqa: BLE001 return 0, str(exc).encode() def get(path, timeout=10.0): req = urllib.request.Request(BASE + path, headers=UA) try: with urllib.request.urlopen(req, timeout=timeout) as resp: return resp.status, resp.read() except urllib.error.HTTPError as exc: return exc.code, exc.read() except Exception as exc: # noqa: BLE001 return 0, str(exc).encode() class Watch: def __init__(self, session): self.session = session self.buf = b"" def send(self, obj): payload = (json.dumps(obj, ensure_ascii=False) + "\n").encode() st, _ = post("/v1/client/send", payload, {"X-Session": self.session}) return st == 200 def reply(self, timeout=25.0): deadline = time.time() + timeout while time.time() < deadline: while b"\n" in self.buf: line, _, self.buf = self.buf.partition(b"\n") if not line.strip(): continue msg = json.loads(line.decode("utf-8")) if "event" in msg: continue return msg st, body = post("/v1/client/poll", b"", {"X-Session": self.session}, timeout=max(4.0, deadline - time.time())) if st != 200: time.sleep(0.4) continue self.buf += body return None def main() -> int: work = Path(tempfile.mkdtemp(prefix="agentrepro_")) relay_proc = agent_proc = None real_data = WINDOWS / "data" backup = WINDOWS / "data_bk_agentrepro" had = real_data.exists() if had: if backup.exists(): shutil.rmtree(backup, ignore_errors=True) shutil.move(str(real_data), str(backup)) try: real_data.mkdir(parents=True, exist_ok=True) sys.path.insert(0, str(WINDOWS)) from lanremote.config import Store store = Store(real_data) store.set_pairing_code(PAIR) store.set("setup_done", True) store.set("allow_new_pairing", True) store.set("close_pair_after_first", False) store.set("server_name", "复现电脑") store.set("relay_url", f"http://127.0.0.1:{RELAY_PORT}") store.set("port", AGENT_PORT) print(" 被控端配置: relay_url=%s port=%s 码=%s" % (store.get("relay_url"), AGENT_PORT, PAIR)) rdata = work / "r" rdata.mkdir(parents=True, exist_ok=True) env = dict(os.environ) env.update({"RELAY_PORT": str(RELAY_PORT), "RELAY_BIND": "127.0.0.1", "RELAY_DATA": str(rdata), "RELAY_POLL": "5", "PYTHONIOENCODING": "utf-8", "PYTHONUNBUFFERED": "1"}) relay_log = open(work / "relay.log", "wb") relay_proc = subprocess.Popen( [sys.executable, "-u", str(ROOT / "relay" / "relay.py")], env=env, stdout=relay_log, stderr=subprocess.STDOUT) for _ in range(40): time.sleep(0.25) if get("/v1/health")[0] == 200: break print(" 中转起来了") agent_log = open(work / "agent.log", "wb") agent_proc = subprocess.Popen( [sys.executable, "-u", str(WINDOWS / "run_agent.py"), "--silent", "--port", str(AGENT_PORT)], cwd=str(WINDOWS), stdout=agent_log, stderr=subprocess.STDOUT) device = None for _ in range(40): time.sleep(0.5) st, body = get("/v1/health") if st == 200: snap = json.loads(body.decode()) if snap.get("devices"): device = snap["devices"][0] break if not device: print(" ✗ 被控端没登记上来") return 1 print(" 被控端已登记: %s 码 %s 在线 %s" % (device["name"], device["code"], device["online"])) st, body = post("/v1/client/attach", json.dumps( {"code": PAIR, "name": "复现手表"}).encode(), {"Content-Type": "application/json"}) att = json.loads(body.decode()) print(" 手表接入: ok=%s" % att.get("ok")) watch = Watch(att.get("session", "")) steps = [ {"cmd": "pair", "deviceId": "watch-repro", "name": "复现手表", "code": PAIR}, {"cmd": "ping"}, {"cmd": "ping"}, {"cmd": "feature_list"}, {"cmd": "info"}, ] for i, msg in enumerate(steps, 1): t0 = time.time() sent = watch.send(msg) got = watch.reply(timeout=25.0) dt = int((time.time() - t0) * 1000) status = "OK" if got is not None else "**超时**" print(" %d. %-14s 发送=%s %s %dms %s" % (i, msg.get("cmd"), sent, status, dt, ("回复字段: " + ",".join(sorted(got.keys()))[:70]) if got else "")) st, body = get("/v1/health") snap = json.loads(body.decode()) d = (snap.get("devices") or [{}])[0] print(" 中转侧: 在线=%s 待发机=%s 待发表=%s 会话=%s 空闲=%ss" % (d.get("online"), d.get("queuedToAgent"), d.get("queuedToClient"), d.get("sessions"), d.get("idleSeconds"))) if got is None: # **一超时就立刻 dump 被控端日志** —— # 否则脚本自己也会卡在后面的步骤,什么都看不到。 agent_log.flush() print(" ════ 超时!被控端日志(最后 25 行)════") try: txt = (work / "agent.log").read_text( encoding="utf-8", errors="replace") for line in txt.splitlines()[-25:]: print(" " + line[:120]) except Exception as exc: # noqa: BLE001 print(" 读日志失败: %s" % exc) print(" ════ 被控端进程还活着吗 ════") print(" poll() = %s" % agent_proc.poll()) print(" ════ 中转看到的设备详情 ════") st, body = get("/v1/health") print(" " + json.dumps( json.loads(body.decode()), ensure_ascii=False)[:400]) break print() print(" ════ 被控端日志 ════") agent_log.flush() txt = (work / "agent.log").read_text(encoding="utf-8", errors="replace") for line in txt.splitlines()[-30:]: print(" " + line[:120]) print() print(" ════ 中转日志 ════") relay_log.flush() txt = (work / "relay.log").read_text(encoding="utf-8", errors="replace") for line in txt.splitlines()[-18:]: print(" " + line[:120]) return 0 finally: for p in (agent_proc, relay_proc): if p is not None: try: p.terminate() p.wait(timeout=6) except Exception: # noqa: BLE001 try: p.kill() except Exception: # noqa: BLE001 pass shutil.rmtree(work, ignore_errors=True) shutil.rmtree(real_data, ignore_errors=True) if had: shutil.move(str(backup), str(real_data)) if __name__ == "__main__": sys.exit(main())