# -*- coding: utf-8 -*- """修 HeadlessUI 的线程模型。 问题:Tk 解释器在主线程创建,却在泵线程里 update()。 Tkinter 有线程亲和性 —— 泵线程第一次 update 就抛异常然后静默 return, 线程直接死了,之后所有 ui_call 都会"界面响应超时"。 修法:整条链路都归泵线程 —— 由它创建 Tk、由它 update、由它执行任务。 主线程只负责往队列里丢活儿。 """ from __future__ import annotations import sys from pathlib import Path HEADLESS = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\windows" r"\lanremote\headless.py") # ---------------- __init__ 加 _ready ---------------- INIT_OLD = ''' self._pump_thread: threading.Thread | None = None self._stopping = False''' INIT_NEW = ''' self._pump_thread: threading.Thread | None = None self._ready = threading.Event() self._stopping = False''' # ---------------- 替换 _ensure_root / _start_pump / ui_call ---------------- OLD = ''' def _ensure_root(self): """按需创建一个**隐藏**的 Tk 根窗口。 只作为内容窗口的父容器,从不显示自己 —— 所以启动时看不到任何窗口。 """ if not IS_WINDOWS or tk is None: return None with self._tk_lock: if self._root is not None: return self._root try: root = tk.Tk() root.withdraw() root.title("") self._root = root self._start_pump() return root except Exception as exc: # noqa: BLE001 self.log(f"创建界面根窗口失败: {exc}") return None def _start_pump(self) -> None: if self._pump_thread is not None: return def pump(): while not self._stopping: try: task = self._queue.get(timeout=0.2) except queue.Empty: if self._root is not None: try: self._root.update() except Exception: # noqa: BLE001 return continue try: task() except Exception as exc: # noqa: BLE001 self.log(f"界面任务出错: {exc}") if self._root is not None: try: self._root.update() except Exception: # noqa: BLE001 return self._pump_thread = threading.Thread(target=pump, daemon=True, name="headless-ui") self._pump_thread.start() def ui_call(self, fn, timeout: float = 8.0): """在界面线程里执行 fn。 静默模式没有独立的主循环线程可依赖,所以:能就地执行就就地执行, 需要 Tk 窗口的操作丢给泵线程并等结果。 """ if self._root is None: # 还没有根窗口;如果这个操作需要 Tk,就先建一个 try: return fn() except Exception as exc: # noqa: BLE001 raise RuntimeError(f"界面不可用: {exc}") from exc result: list = [] error: list = [] done = threading.Event() def task(): try: result.append(fn()) except Exception as exc: # noqa: BLE001 error.append(exc) finally: done.set() self._queue.put(task) if not done.wait(timeout): raise TimeoutError("界面响应超时") if error: raise error[0] return result[0] if result else None''' NEW = ''' def _ensure_root(self): """按需启动界面线程,并等它把**隐藏的** Tk 根窗口建好。 关键:Tk 解释器必须在**同一个线程**里创建和使用。 所以根窗口由泵线程自己建 —— 如果在主线程建、在泵线程 update, Tkinter 会因为线程亲和性直接抛异常,泵线程当场就死了。 """ if not IS_WINDOWS or tk is None: return None if threading.current_thread() is self._pump_thread: return self._root # 已经在界面线程里 with self._tk_lock: if self._root is not None: return self._root if self._pump_thread is None: self._ready.clear() self._pump_thread = threading.Thread( target=self._pump, daemon=True, name="headless-ui") self._pump_thread.start() self._ready.wait(8.0) return self._root def _pump(self) -> None: """界面线程:Tk 在这里建,也只有这里能碰它。""" try: root = tk.Tk() root.withdraw() root.title("") except Exception as exc: # noqa: BLE001 self.log(f"创建界面根窗口失败: {exc}") self._ready.set() return self._root = root self._ready.set() while not self._stopping: try: task = self._queue.get(timeout=0.05) except queue.Empty: task = None if task is not None: try: task() except Exception as exc: # noqa: BLE001 self.log(f"界面任务出错: {exc}") try: root.update() except Exception: # noqa: BLE001 break try: root.destroy() except Exception: # noqa: BLE001 pass self._root = None def ui_call(self, fn, timeout: float = 8.0): """在界面线程里执行 fn。 Tk 只允许在拥有它的那个线程里操作,所以这里一律丢给泵线程; 调用方本来就在泵线程里的话就地执行(否则会自己等自己)。 """ if threading.current_thread() is self._pump_thread: return fn() root = self._ensure_root() if root is None: return fn() # 没有 Tk 环境(非 Windows),就地执行 result: list = [] error: list = [] done = threading.Event() def task(): try: result.append(fn()) except Exception as exc: # noqa: BLE001 error.append(exc) finally: done.set() self._queue.put(task) if not done.wait(timeout): raise TimeoutError("界面响应超时") if error: raise error[0] return result[0] if result else None''' def main() -> int: text = HEADLESS.read_text(encoding="utf-8") ok = True for old, new, tag in ((INIT_OLD, INIT_NEW, "_ready 字段"), (OLD, NEW, "线程模型重写")): if old not in text: print(f" [跳过] {tag} —— 锚点没找到") ok = False continue text = text.replace(old, new, 1) print(f" {tag}: OK") if ok: HEADLESS.write_text(text, encoding="utf-8") import ast try: ast.parse(HEADLESS.read_text(encoding="utf-8")) print(" headless.py: 语法 OK") except SyntaxError as exc: print(f" headless.py: 语法错误 {exc}") ok = False return 0 if ok else 1 if __name__ == "__main__": sys.exit(main())