优化逻辑

This commit is contained in:
2025-11-24 20:38:50 +08:00
parent af4ab583a5
commit 22da742532
2 changed files with 323 additions and 222 deletions

View File

@@ -7,11 +7,11 @@ import subprocess
from typing import Dict, Optional
import tidevice
# import wda # 目前先不用 wda 获取屏幕,避免触发 tidevice 的那套 WDA 启动
import wda
from tidevice import Usbmux, ConnectionType
from Entity.DeviceModel import DeviceModel
from Entity.Variables import WdaAppBundleId, wdaScreenPort, wdaFunctionPort
from Entity.Variables import WdaAppBundleId, wdaFunctionPort
from Module.FlaskSubprocessManager import FlaskSubprocessManager
from Module.IOSActivator import IOSActivator
from Utils.LogManager import LogManager
@@ -21,6 +21,9 @@ class DeviceInfo:
_instance = None
_instance_lock = threading.Lock()
# 离线宽限期
REMOVE_GRACE_SEC = float(os.getenv("REMOVE_GRACE_SEC", "6.0"))
def __new__(cls, *args, **kwargs):
if not cls._instance:
with cls._instance_lock:
@@ -28,8 +31,6 @@ class DeviceInfo:
cls._instance = super().__new__(cls)
return cls._instance
REMOVE_GRACE_SEC = float(os.getenv("REMOVE_GRACE_SEC", "6.0"))
def __init__(self) -> None:
if getattr(self, "_initialized", False):
return
@@ -55,7 +56,7 @@ class DeviceInfo:
self._creationflags = 0
si = subprocess.STARTUPINFO()
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore[attr-defined]
si.wShowWindow = 0 # SW_HIDE
self._startupinfo = si
@@ -63,9 +64,9 @@ class DeviceInfo:
print("[Init] DeviceInfo 初始化完成")
self._initialized = True
# ---------------------------
# ==========================
# 主循环
# ---------------------------
# ==========================
def listen(self):
LogManager.method_info("进入主循环", "listen", udid="system")
print("[Listen] 开始监听设备上下线...")
@@ -82,18 +83,18 @@ class DeviceInfo:
with self._lock:
known = set(self._models.keys())
# ---------- 1. 新设备 ----------
# 1. 新设备
for udid in online:
self._last_seen[udid] = time.time()
if udid not in known:
try:
self._add_device(udid)
except Exception as e:
# 关键:单设备异常不能干掉整个循环
# 单设备异常不能干掉整个循环
LogManager.warning(f"[Add] 处理设备 {udid} 异常: {e}", udid=udid)
print(f"[Add] 处理设备 {udid} 异常: {e}")
# ---------- 2. 可能离线设备 ----------
# 2. 可能离线设备
now = time.time()
for udid in list(known):
if udid not in online:
@@ -107,9 +108,9 @@ class DeviceInfo:
time.sleep(1)
# ---------------------------
# ==========================
# 添加设备
# ---------------------------
# ==========================
def _add_device(self, udid: str):
with self._lock:
if udid in self._models:
@@ -125,20 +126,7 @@ class DeviceInfo:
print(f"[Add] 获取系统版本失败 {udid}: {e}")
version_major = 0
# 启动 WDA
if version_major >= 17.0:
threading.Thread(
target=IOSActivator().activate_ios17,
args=(udid,),
daemon=True,
).start()
else:
try:
tidevice.Device(udid).app_start(WdaAppBundleId)
except Exception as e:
print(f"[Add] 使用 tidevice 启动 WDA 失败 {udid}: {e}")
# 分配投屏端口 & 写入模型
# 分配投屏端口 & 写入模型先插入width/height=0后面再异步更新
with self._lock:
self.screenPort += 1
screen_port = self.screenPort
@@ -149,7 +137,7 @@ class DeviceInfo:
width=0,
height=0,
scale=0,
type=1
type=1,
)
self._models[udid] = model
@@ -162,16 +150,106 @@ class DeviceInfo:
except Exception as e:
print(f"[iproxy] 启动失败 {udid}: {e}")
# 异步等待 WDA + 更新屏幕尺寸(后面你要真用尺寸,再把 _screen_info 换成 wda 版本即可)
# 启动 WDA
if version_major >= 17.0:
# iOS17+ 走 go-ios传入回调WDA 启动后再拿屏幕尺寸
threading.Thread(
target=IOSActivator().activate_ios17,
args=(udid, self._on_wda_ready),
daemon=True,
).start()
else:
# 旧版本直接用 tidevice 启动 WDA然后异步获取屏幕尺寸
try:
tidevice.Device(udid).app_start(WdaAppBundleId)
except Exception as e:
print(f"[Add] 使用 tidevice 启动 WDA 失败 {udid}: {e}")
else:
threading.Thread(
target=self._fetch_screen_and_notify,
args=(udid,),
daemon=True,
).start()
# ==========================
# WDA 启动回调iOS17+
# ==========================
def _on_wda_ready(self, udid: str):
print(f"[WDA] 回调触发,准备获取屏幕信息 udid={udid}")
# 稍微等一下再连,避免刚启动时不稳定
time.sleep(1)
threading.Thread(
target=self._async_wait_wda_and_update_screen,
target=self._fetch_screen_and_notify,
args=(udid,),
daemon=True
daemon=True,
).start()
# ---------------------------
# 启动 iproxy投屏
# ---------------------------
# ==========================
# 通过 WDA 获取屏幕信息
# ==========================
def _screen_info(self, udid: str):
try:
# 用 USBClient通过 WDA 功能端口访问
c = wda.USBClient(udid, wdaFunctionPort)
size = c.window_size()
w = int(size.width)
h = int(size.height)
s = float(c.scale) # facebook-wda 的 scale 挂在 client 上
print(f"[Screen] 成功获取屏幕 {w}x{h} scale={s} {udid}")
return w, h, s
except Exception as e:
print(f"[Screen] 获取屏幕失败: {e} udid={udid}")
return 0, 0, 0.0
# ==========================
# 异步获取屏幕尺寸并通知 Flask
# ==========================
def _fetch_screen_and_notify(self, udid: str):
"""
后台线程里多次尝试通过 WDA 获取屏幕尺寸,
成功后更新 model 并发一次 snapshot。
"""
max_retry = 15
interval = 1.0
# 给 WDA 一点启动缓冲时间
time.sleep(2.0)
for _ in range(max_retry):
# 设备已移除就不再尝试
with self._lock:
if udid not in self._models:
print(f"[Screen] 设备已移除,停止获取屏幕信息 udid={udid}")
return
w, h, s = self._screen_info(udid)
if w > 0 and h > 0:
# 更新模型
with self._lock:
m = self._models.get(udid)
if not m:
print(f"[Screen] 模型已不存在,无法更新 udid={udid}")
return
m.width = w
m.height = h
m.scale = s
print(f"[Screen] 屏幕信息更新完成,准备推送到 Flask udid={udid}")
try:
self._manager_send()
except Exception as e:
print(f"[Screen] 发送屏幕更新到 Flask 失败 udid={udid}, err={e}")
return
time.sleep(interval)
print(f"[Screen] 多次尝试仍未获取到屏幕信息 udid={udid}")
# ==========================
# iproxy 管理
# ==========================
def _start_iproxy(self, udid: str, local_port: int):
iproxy_path = self._find_iproxy()
@@ -183,14 +261,14 @@ class DeviceInfo:
args = [
iproxy_path,
"-u", udid,
str(local_port), # 本地端口(投屏)
"9567" # 手机端口(go-ios screencast
"-u",
udid,
str(local_port), # 本地端口(投屏
"9567", # 手机端口go-ios screencast
]
print(f"[iproxy] 启动进程: {args}")
# 不用 PIPE防止没人读导致缓冲爆掉窗口用前面配置隐藏
proc = subprocess.Popen(
args,
stdout=subprocess.DEVNULL,
@@ -198,12 +276,8 @@ class DeviceInfo:
creationflags=self._creationflags,
startupinfo=self._startupinfo,
)
self._iproxy_process[udid] = proc
# ---------------------------
# 停止 iproxy
# ---------------------------
def _stop_iproxy(self, udid: str):
p = self._iproxy_process.get(udid)
if not p:
@@ -219,39 +293,13 @@ class DeviceInfo:
self._iproxy_process.pop(udid, None)
print(f"[iproxy] 已停止 {udid}")
# ---------------------------
# 异步等待 WDA 然后更新屏幕大小
# ---------------------------
def _async_wait_wda_and_update_screen(self, udid: str):
print(f"[WDA] 等待 WDA 就绪 {udid}")
try:
# 最长等待 20 秒(你后面要真用屏幕尺寸,再把 _screen_info 换回 wda 的实现)
for _ in range(20):
w, h, s = self._screen_info(udid)
if w > 0:
print(f"[WDA] 屏幕信息成功 {udid} {w}x{h} scale={s}")
with self._lock:
m = self._models.get(udid)
if m:
m.width = w
m.height = h
m.scale = s
self._manager_send()
return
time.sleep(1)
except Exception as e:
print(f"[WDA] 获取屏幕信息异常 {udid}: {e}")
print(f"[WDA] 屏幕信息获取失败(超时) {udid}")
# ---------------------------
# ==========================
# 移除设备
# ---------------------------
# ==========================
def _remove_device(self, udid: str):
print(f"[Remove] 移除设备 {udid}")
# 先停 iproxy(只管自己的,不影响其他设备)
# 先停 iproxy
self._stop_iproxy(udid)
with self._lock:
@@ -260,34 +308,17 @@ class DeviceInfo:
self._manager_send()
# ---------------------------
# WDA 屏幕查询(当前保持空实现)
# ---------------------------
def _screen_info(self, udid: str):
# 现在先不通过 wda 取屏幕,避免触发 tidevice 那套 WDA 启动逻辑
# 你如果确认 go-ios 跑起来后用 facebook-wda 取尺寸是安全的,可以改回下面这种:
#
# try:
# c = wda.USBClient(udid, wdaFunctionPort)
# size = c.window_size()
# return int(size.width), int(size.height), float(c.scale)
# except Exception as e:
# print(f"[Screen] 获取屏幕信息异常: {e} {udid}")
# return 0, 0, 0.0
return 0, 0, 0.0
# ---------------------------
# 找 iproxy
# ---------------------------
# ==========================
# 工具方法
# ==========================
def _find_iproxy(self) -> str:
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
name = "iproxy.exe" if os.name == "nt" else "iproxy"
path = os.path.join(base, "resources", "iproxy", name)
return path
return os.path.join(base, "resources", "iproxy", name)
# ---------------------------
# 数据同步到 Flask
# ---------------------------
# ==========================
# 同步数据到 Flask
# ==========================
def _manager_send(self):
try:
self._send_snapshot_to_flask()
@@ -306,8 +337,8 @@ class DeviceInfo:
devices = [m.toDict() for m in self._models.values()]
payload = json.dumps({"devices": devices}, ensure_ascii=False)
port = int(os.getenv("FLASK_COMM_PORT", "34566"))
with socket.create_connection(("127.0.0.1", port), timeout=1.5) as s:
s.sendall(payload.encode() + b"\n")