Files
ykxmz/windows/lanremote/headless.py
T
xmz7891 b705216e4a ykxmz 1.5.0锛氫慨澶嶆墜琛ㄧ TLS 璇佷功鏍¢獙 + MTU 淇缃戠粶绋冲畾鎬?
琚帶绔?- 淇 MTU 闂锛氬す MSS + 璺緞 MTU 鎺㈡祴锛堝叕缃戝彲鐢ㄧ巼 48% -> 100%锛?- 澶嶇敤 HTTP 杩炴帴锛堝厤鎺夋瘡娆℃柊寤?TLS 鐨?30% 鎻℃墜澶辫触锛?- 姣忓垎閽熷洖澶存壘灞€鍩熺綉锛岃嚜鍔ㄥ垏鍥炲揩 400 鍊嶇殑璺緞
- 鐢婚潰蹇冭烦甯э細闈欐鏃剁渷娴侀噺锛屽鎴风浠嶇煡閬撴祦鏄椿鐨?- 閿佸睆鏃剁粰鍑哄彲鎿嶄綔鐨勬彁绀?
鎵嬭〃绔?- 鍐呯疆 GTS Root R4 / GlobalSign Root CA锛屼慨 SSLHandshakeException
- 杩炴帴閿欒缈昏瘧鎴愪汉璇濓紙璇佷功/鏃堕棿/瑙f瀽/瓒呮椂鍒嗗埆鎻愮ず锛?- 缁忎腑杞繛涓婂悗鐢ㄦ湇鍔″櫒鎶ョ殑鍦板潃鍒锋柊宸查厤瀵硅褰?- 鏂板灞€鍩熺綉杩炴帴椤甸潰銆佹湇鍔″櫒寤惰繜妫€娴?
鏈嶅姟鍣ㄧ
- 鏀寔 chunked 璇锋眰浣擄紙淇瘡 16 绉掍竴娆$殑杩炴帴閿欎贡涓庡ぇ闈㈢Н 502锛?- 缃戠粶璋冧紭鑴氭湰 net-tune.sh锛堝紑鏈鸿嚜鍔ㄥ簲鐢級
- 绠$悊椤碉細鎵归噺鍒犻櫎銆佺綉缁滀綋妫€銆佷笅杞藉尯
- 闅ч亾鏀?HTTP/2 + 鍙岃繛鎺?
鏂囨。
- 璁稿彲鍗忚 / 瀹夎鍓嶅繀璇?/ 瀹夎鍚庨』鐭ワ紙浣滆€咃細灏忛害瀛愶級
2026-10-06 19:01:05 +08:00

513 lines
19 KiB
Python

# -*- coding: utf-8 -*-
"""静默运行模式:启动时没有任何窗口。
和 AgentUI 的区别:
没有主控制窗口、没有托盘、没有终端。
但 **屏幕共享提示保留** —— 有人在看你的屏幕时,屏幕右下角会出现一条提示。
这在有配对认证的前提下依然保留,理由:让电脑前面的人知道"此刻有人在看"。
内容窗口(投图、全屏大字、白板)是**手表明确要求显示**的,所以照常工作 ——
它们不是"程序自己弹出来的窗口"。Tk 根窗口按需惰性创建并保持隐藏。
没有 Tk 也能用的能力(剪贴板、屏幕分辨率)直接用 Win32 API 实现,
不依赖界面。
"""
from __future__ import annotations
import ctypes
import queue
import threading
import time
from pathlib import Path
IS_WINDOWS = False
try:
import tkinter as tk
IS_WINDOWS = True
except Exception: # noqa: BLE001
tk = None
# ======================================================================
# 不依赖 Tk 的系统能力
# ======================================================================
class _Clipboard:
"""直接读写 Windows 剪贴板,不需要 Tk。"""
CF_UNICODETEXT = 13
GMEM_MOVEABLE = 0x0002
@classmethod
def get(cls) -> str:
if not IS_WINDOWS:
return ""
user32 = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32
# 64 位下必须声明,否则句柄会被截断
user32.GetClipboardData.restype = ctypes.c_void_p
kernel32.GlobalLock.restype = ctypes.c_void_p
kernel32.GlobalLock.argtypes = [ctypes.c_void_p]
kernel32.GlobalUnlock.argtypes = [ctypes.c_void_p]
if not user32.OpenClipboard(None):
return ""
try:
handle = user32.GetClipboardData(cls.CF_UNICODETEXT)
if not handle:
return ""
pointer = kernel32.GlobalLock(handle)
if not pointer:
return ""
try:
return ctypes.c_wchar_p(pointer).value or ""
finally:
kernel32.GlobalUnlock(handle)
finally:
user32.CloseClipboard()
@classmethod
def set(cls, text: str) -> bool:
if not IS_WINDOWS:
return False
user32 = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32
kernel32.GlobalAlloc.restype = ctypes.c_void_p
kernel32.GlobalAlloc.argtypes = [ctypes.c_uint, ctypes.c_size_t]
kernel32.GlobalLock.restype = ctypes.c_void_p
kernel32.GlobalLock.argtypes = [ctypes.c_void_p]
kernel32.GlobalUnlock.argtypes = [ctypes.c_void_p]
user32.SetClipboardData.restype = ctypes.c_void_p
user32.SetClipboardData.argtypes = [ctypes.c_uint, ctypes.c_void_p]
data = (text + "\0").encode("utf-16-le")
handle = kernel32.GlobalAlloc(cls.GMEM_MOVEABLE, len(data))
if not handle:
return False
pointer = kernel32.GlobalLock(handle)
if not pointer:
return False
ctypes.memmove(pointer, data, len(data))
kernel32.GlobalUnlock(handle)
if not user32.OpenClipboard(None):
return False
try:
user32.EmptyClipboard()
return bool(user32.SetClipboardData(cls.CF_UNICODETEXT, handle))
finally:
user32.CloseClipboard()
# ======================================================================
# 静默 UI
# ======================================================================
class HeadlessUI:
"""给静默模式用的界面替身。
接口和 AgentUI 对齐,这样 Actions / RemoteServer 一行都不用改。
"""
def __init__(self, store, log_file: Path | None = None,
allow_content_windows: bool = True):
self.store = store
self.log_file = Path(log_file) if log_file else None
self.allow_content_windows = allow_content_windows
self._root = None # 惰性创建,见下面的 root 属性
self._tk_ready = False
self._tk_lock = threading.RLock()
self._ui_lock = threading.RLock()
self._queue: queue.Queue = queue.Queue()
self.server = None
self.state = "stopped"
self.port = 0
self._screen_on = False
self._notices = []
self._notice = None
self._notice_inner = None
self.mirroring = False
self._pump_thread: threading.Thread | None = None
self._ready = threading.Event()
self._stopping = False
# ---------------- 日志 ----------------
def log(self, message: str) -> None:
line = f"[{time.strftime('%H:%M:%S')}] {message}"
if self.log_file:
try:
with self.log_file.open("a", encoding="utf-8") as handle:
handle.write(line + "\n")
except OSError:
pass
# 别名,有些地方用 set_status
def set_status(self, text: str) -> None:
self.log(text)
def notify(self, title: str, text: str) -> None:
"""弹一个小通知窗(屏幕右下角,几秒后自动消失)。
之前这里只写日志,所以手表点「弹通知」电脑上毫无反应。
静默模式的意思是"开机不弹控制窗口",而不是
"手表明确要求显示的东西也不显示" —— 投图、全屏大字、通知
都属于"明确要求",照常显示。
"""
self.log(f"[通知] {title}: {text}")
root = self._ensure_root()
if root is None:
return
def build():
try:
win = tk.Toplevel(root)
win.title(f"通知 - {title}")
win.overrideredirect(True)
win.attributes("-topmost", True)
win.configure(bg="#ffffff")
box = tk.Frame(win, bg="#ffffff", highlightthickness=1,
highlightbackground="#d0d7de")
box.pack(fill="both", expand=True)
tk.Frame(box, bg="#185ce8", height=4).pack(fill="x")
tk.Label(box, text=title or "通知", bg="#ffffff",
fg="#1f2328", font=("Microsoft YaHei UI", 11, "bold"),
anchor="w").pack(fill="x", padx=14, pady=(10, 2))
tk.Label(box, text=text or "", bg="#ffffff", fg="#656d76",
font=("Microsoft YaHei UI", 10), anchor="w",
justify="left", wraplength=260).pack(
fill="x", padx=14, pady=(0, 12))
win.update_idletasks()
screen_w, screen_h = self.screen_size()
width = max(240, win.winfo_reqwidth())
height = win.winfo_reqheight()
win.geometry(f"+{screen_w - width - 24}+{screen_h - height - 72}")
def close(_event=None):
try:
win.destroy()
except Exception: # noqa: BLE001
pass
win.bind("<Button-1>", close)
win.after(6000, close)
except Exception as exc: # noqa: BLE001
self.log(f"通知显示失败: {exc}")
self._notices.append(None) # 先占位,build 里回填
index = len(self._notices) - 1
def build():
try:
win = tk.Toplevel(root)
win.title(f"通知 - {title}")
win.overrideredirect(True)
win.attributes("-topmost", True)
win.configure(bg="#ffffff")
self._notices[index] = win
box = tk.Frame(win, bg="#ffffff", highlightthickness=1,
highlightbackground="#d0d7de")
box.pack(fill="both", expand=True)
tk.Frame(box, bg="#185ce8", height=4).pack(fill="x")
tk.Label(box, text=title or "通知", bg="#ffffff",
fg="#1f2328", font=("Microsoft YaHei UI", 11, "bold"),
anchor="w").pack(fill="x", padx=14, pady=(10, 2))
tk.Label(box, text=text or "", bg="#ffffff", fg="#656d76",
font=("Microsoft YaHei UI", 10), anchor="w",
justify="left", wraplength=260).pack(
fill="x", padx=14, pady=(0, 12))
win.update_idletasks()
screen_w, screen_h = self.screen_size()
width = max(240, win.winfo_reqwidth())
height = win.winfo_reqheight()
win.geometry(f"+{screen_w - width - 24}+{screen_h - height - 72}")
def close(_event=None):
try:
win.destroy()
except Exception: # noqa: BLE001
pass
try:
self._notices[index] = None
except (IndexError, AttributeError):
pass
win.bind("<Button-1>", close)
win.after(6000, close)
except Exception as exc: # noqa: BLE001
self.log(f"通知显示失败: {exc}")
try:
self._queue.put(build)
except Exception: # noqa: BLE001
pass
def close_notifications(self) -> bool:
"""关掉所有还在显示的通知窗。返回是否关掉了至少一个。"""
handled = False
for index, win in enumerate(list(getattr(self, "_notices", []))):
if win is None:
continue
handled = True
def build(target=win, slot=index):
try:
target.destroy()
except Exception: # noqa: BLE001
pass
try:
self._notices[slot] = None
except (IndexError, AttributeError):
pass
try:
self._queue.put(build)
except Exception: # noqa: BLE001
pass
return handled
# ---------------- Tk 根窗口(惰性、隐藏)----------------
@property
def root(self):
"""Tk 根窗口,按需创建且**始终隐藏**。
AgentUI 已经删掉了,但 Actions 里投图/全屏大字这些还要一个父窗口。
这里惰性建一个 withdrew 的根窗口 —— 它本身永远不显示,
所以启动时屏幕上依然什么都没有。
"""
return self._ensure_root()
@root.setter
def root(self, value):
self._root = value
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 screen_size(self) -> tuple:
if IS_WINDOWS:
try:
user32 = ctypes.windll.user32
return (user32.GetSystemMetrics(0), user32.GetSystemMetrics(1))
except Exception: # noqa: BLE001
pass
return (1920, 1080)
def center_window(self, win, width: int = 0, height: int = 0) -> None:
pass # 静默模式不做居中
# ---------------- 服务端会用到的回调 ----------------
def on_state_changed(self, state: str) -> None:
self.state = state
def flash_screen_indicator(self, seconds: float = 2.5) -> None:
self.log("(截屏)")
def set_screen_sharing(self, on: bool) -> None:
"""屏幕共享提示。
这是**故意保留**的:有人在看你的屏幕时,屏幕右下角会出现一条提示。
静默模式只是不显示主控制窗口,不是"让被看的人不知道"。
"""
if on == self._screen_on:
return
self._screen_on = on
try:
if on:
self._show_notice("屏幕正在被共享")
else:
self._hide_notice()
except Exception as exc: # noqa: BLE001
self.log(f"共享提示失败: {exc}")
def refresh_sharing_notice(self, status: dict, sharing: bool) -> None:
self.set_screen_sharing(sharing)
def refresh(self) -> None:
pass
def open_diagnostics(self, from_watch: str = "") -> None:
"""手表请求自检:静默模式下不弹窗口,只记日志。"""
self.log(f"手表 {from_watch or '?'} 请求了网络自检(静默模式不弹窗口)")
def _show_notice(self, text: str) -> None:
root = self._ensure_root()
if root is None:
return
def build():
win = tk.Toplevel(root)
# 标题要设 —— 无边框窗口在外观上看不到,
# 但系统里能查到这个标题,是"确实显示了提示"的可验证证据。
win.title(text)
win.overrideredirect(True)
win.attributes("-topmost", True)
win.configure(bg="#cf222e")
label = tk.Label(win, text=text, bg="#cf222e", fg="#ffffff",
font=("Microsoft YaHei UI", 11, "bold"),
padx=16, pady=8)
label.pack()
screen_w, screen_h = self.screen_size()
win.update_idletasks()
width = win.winfo_reqwidth()
height = win.winfo_reqheight()
win.geometry(f"+{screen_w - width - 24}+{screen_h - height - 72}")
self._notice = win
try:
self._queue.put(build)
except Exception: # noqa: BLE001
pass
def _hide_notice(self) -> None:
win = self._notice
self._notice = None
if win is None:
return
def destroy():
try:
win.destroy()
except Exception: # noqa: BLE001
pass
try:
self._queue.put(destroy)
except Exception: # noqa: BLE001
pass
# ---------------- 设置窗口等:静默模式下没有 ----------------
def open_settings(self) -> None:
self.log("静默模式没有设置窗口,请改用带界面的模式或直接编辑 data/settings.json")
def open_help(self) -> None:
self.log("静默模式没有说明窗口")
def open_log(self) -> None:
self.log("静默模式没有日志窗口;日志写在 data/silent.log")
def show_message(self, text: str) -> None:
self.log(text)
def show_text_window(self, title: str, text: str) -> None:
self.log(f"[{title}] {text}")
def quit_app(self) -> None:
self.stop()
def stop(self) -> None:
self._stopping = True
# 不要在非界面线程里 destroy 根窗口 —— Tcl 会报
# "async handler deleted by the wrong thread"。
# 让泵线程自己收尾(它的循环退出时会 destroy)。
self._hide_notice()
thread = self._pump_thread
if thread is not None and thread.is_alive():
thread.join(timeout=2.0)
self._root = None
# ---------------- 静默模式自己的控制 ----------------
def run_forever(self) -> None:
"""一直跑着,直到收到停止信号。"""
while not self._stopping:
time.sleep(0.5)