加固视频流处理。当前版本为稳定版

This commit is contained in:
2025-12-04 14:20:53 +08:00
parent 746314f0ff
commit 24082dc2a4

View File

@@ -19,8 +19,8 @@ class DeviceInfo:
_instance = None
_instance_lock = threading.Lock()
# 离线宽限期
REMOVE_GRACE_SEC = 3.0
# 离线宽限期(保持你原来的数值)
REMOVE_GRACE_SEC = 5.0
def __new__(cls, *args, **kwargs):
if not cls._instance:
@@ -44,12 +44,16 @@ class DeviceInfo:
# iproxy 子进程udid -> Popen
self._iproxy_process: Dict[str, subprocess.Popen] = {}
# iproxy HTTP 健康检查失败次数udid -> 连续失败次数
self._iproxy_fail_count: Dict[str, int] = {}
# Windows 下隐藏子进程窗口(给 iproxy 用)
self._creationflags = 0
self._startupinfo = None
if os.name == "nt":
try:
self._creationflags = subprocess.CREATE_NO_WINDOW # type: ignore[attr-defined]
# type: ignore[attr-defined]
self._creationflags = subprocess.CREATE_NO_WINDOW
except Exception:
self._creationflags = 0
@@ -62,9 +66,6 @@ class DeviceInfo:
print("[Init] DeviceInfo 初始化完成")
self._initialized = True
# ==========================
# 主循环
# ==========================
# ==========================
# 主循环
# ==========================
@@ -87,52 +88,62 @@ class DeviceInfo:
# 当前已知的设备(本轮循环开始时)
with self._lock:
known = set(self._models.keys())
# 当前已经维护的设备数量
current_count = len(self._models)
# 1. 处理在线设备
for udid in online:
# 更新心跳时间(只要在 online 里,都记一笔,方便离线判断)
# 更新心跳时间
self._last_seen[udid] = now
# 如果是新设备并且数量已经 >= 6直接忽略
# 新设备数量已达上限
if udid not in known and current_count >= 6:
print(f"[Add] 设备数量已达 6 台,忽略新设备: {udid}")
LogManager.info(f"[Add] 设备数量已达上限(6),忽略新设备", udid=udid)
LogManager.info(
"[Add] 设备数量已达上限(6),忽略新设备",
udid=udid,
)
continue
# 已经在列表里的设备,直接跳过“是否信任”检查和添加流程
# 已经在列表里的设备,跳过添加流程
if udid in known:
continue
# 只对新发现的设备做一次信任检查
# 只对新发现的设备做一次信任检查
try:
if not self._is_trusted(udid):
# 未信任 / 未配对 / 暂时不可用,本轮直接跳过
LogManager.info(f"[Add] 设备未信任或未就绪,跳过本轮添加: {udid}", udid=udid)
LogManager.info(
"[Add] 设备未信任或未就绪,跳过本轮添加",
udid=udid,
)
print(f"[Add] 设备未信任或未就绪,跳过: {udid}")
continue
except Exception as e:
# 信任检查本身异常,也当作暂时未就绪处理
LogManager.warning(f"[Add] 检测设备 {udid} 信任状态异常: {e}", udid=udid)
LogManager.warning(
f"[Add] 检测设备 {udid} 信任状态异常: {e}",
udid=udid,
)
print(f"[Add] 检测设备 {udid} 信任状态异常: {e}")
continue
# 次确认一下数量(极端情况下,多线程同时改 models 的话更稳妥一点)
# 次确认数量上限
with self._lock:
if len(self._models) >= 6:
print(f"[Add] 二次检查: 设备数量已达 6 台,忽略新设备: {udid}")
LogManager.info(f"[Add] 二次检查数量上限,忽略新设备", udid=udid)
LogManager.info(
"[Add] 二次检查数量上限,忽略新设备",
udid=udid,
)
continue
# 确认“已信任”的新设备,才真正走 _add_device
# 真正添加设备
try:
self._add_device(udid)
# 本轮循环内的计数也 +1防止这一轮里连续加超过 6 台
current_count += 1
except Exception as e:
# 单设备异常不能干掉整个循环
LogManager.warning(f"[Add] 处理设备 {udid} 异常: {e}", udid=udid)
LogManager.warning(
f"[Add] 处理设备 {udid} 异常: {e}",
udid=udid,
)
print(f"[Add] 处理设备 {udid} 异常: {e}")
# 2. 处理可能离线的设备(只看本轮开始时 known 里的)
@@ -143,26 +154,37 @@ class DeviceInfo:
try:
self._remove_device(udid)
except Exception as e:
LogManager.method_error(f"移除失败:{e}", "listen", udid=udid)
LogManager.method_error(
f"移除失败:{e}",
"listen",
udid=udid,
)
print(f"[Remove] 移除失败 {udid}: {e}")
# 3. iproxy 看门狗(进程 + HTTP 探活)
try:
self._check_iproxy_health()
except Exception as e:
LogManager.warning(
f"[iproxy] 看门狗异常: {e}",
udid="system",
)
print(f"[iproxy] 看门狗异常: {e}")
time.sleep(1)
# 判断设备是否信任
def _is_trusted(self, udid: str) -> bool:
try:
d = tidevice.Device(udid)
# 随便读一个需要 lockdown/配对的字段
_ = d.product_version # 或 d.info视你当前 tidevice 版本而定
_ = d.product_version
return True
except Exception as e:
msg = str(e)
# 这里只是示意,你可以根据你本地真实报错关键字再精细一点
if "NotTrusted" in msg or "Please trust" in msg or "InvalidHostID" in msg:
print(f"[Trust] 设备未信任udid={udid}, err={msg}")
return False
# 如果是别的错误(比如瞬时通信异常),我倾向于当作“暂时不信任”,避免把有问题的设备加进去
print(f"[Trust] 检测信任状态出错,当作未信任处理 udid={udid}, err={msg}")
return False
@@ -184,7 +206,7 @@ class DeviceInfo:
print(f"[Add] 获取系统版本失败 {udid}: {e}")
version_major = 0
# 分配投屏端口 & 写入模型先插入width/height=0后面再异步更新
# 分配投屏端口 & 写入模型
with self._lock:
self.screenPort += 1
screen_port = self.screenPort
@@ -207,21 +229,24 @@ class DeviceInfo:
self._start_iproxy(udid, screen_port)
except Exception as e:
print(f"[iproxy] 启动失败 {udid}: {e}")
LogManager.warning(f"[iproxy] 启动失败: {e}", udid=udid)
# 启动 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}")
LogManager.warning(
f"[Add] 使用 tidevice 启动 WDA 失败: {e}",
udid=udid,
)
else:
threading.Thread(
target=self._fetch_screen_and_notify,
@@ -234,7 +259,6 @@ class DeviceInfo:
# ==========================
def _on_wda_ready(self, udid: str):
print(f"[WDA] 回调触发,准备获取屏幕信息 udid={udid}")
# 稍微等一下再连,避免刚启动时不稳定
time.sleep(1)
threading.Thread(
target=self._fetch_screen_and_notify,
@@ -247,13 +271,12 @@ class DeviceInfo:
# ==========================
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 上
s = float(c.scale)
print(f"[Screen] 成功获取屏幕 {w}x{h} scale={s} {udid}")
return w, h, s
@@ -272,10 +295,8 @@ class DeviceInfo:
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}")
@@ -283,7 +304,6 @@ class DeviceInfo:
w, h, s = self._screen_info(udid)
if w > 0 and h > 0:
# 更新模型
with self._lock:
m = self._models.get(udid)
if not m:
@@ -310,7 +330,6 @@ class DeviceInfo:
def _start_iproxy(self, udid: str, local_port: int):
iproxy_path = self._find_iproxy()
# 已有进程并且还在跑,就不重复起
p = self._iproxy_process.get(udid)
if p is not None and p.poll() is None:
print(f"[iproxy] 已存在运行中的进程,跳过 {udid}")
@@ -318,14 +337,14 @@ class DeviceInfo:
args = [
iproxy_path,
"-u", udid,
"-u",
udid,
str(local_port), # 本地端口(投屏)
"9567" # 手机端口go-ios screencast
"9567", # 手机端口go-ios screencast
]
print(f"[iproxy] 启动进程: {args}")
# 不用 PIPE防止没人读导致缓冲爆掉窗口用前面配置隐藏
proc = subprocess.Popen(
args,
stdout=subprocess.DEVNULL,
@@ -351,18 +370,109 @@ class DeviceInfo:
self._iproxy_process.pop(udid, None)
print(f"[iproxy] 已停止 {udid}")
def _is_iproxy_http_healthy(self, local_port: int, timeout: float = 1.0) -> bool:
"""
通过向本地 iproxy 转发端口发一个最小的 HTTP 请求,
来判断隧道是否“活着”:
- 正常:能在超时时间内读到一些 HTTP 头 / 任意字节;
- 异常:连接失败、超时、完全收不到字节,都认为不健康。
"""
try:
with socket.create_connection(("127.0.0.1", local_port), timeout=timeout) as s:
s.settimeout(timeout)
req = b"GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"
s.sendall(req)
data = s.recv(128)
if not data:
return False
if data.startswith(b"HTTP/") or b"\r\n" in data:
return True
# 即使不是标准 HTTP 头,只要有返回字节,也说明隧道有响应
return True
except (socket.timeout, OSError):
return False
except Exception:
return False
def _check_iproxy_health(self):
"""
iproxy 看门狗:
- 先看进程是否存在 / 已退出;
- 再做一次 HTTP 层探活;
- 连续多次失败才重启,避免抖动时频繁重启。
"""
with self._lock:
items = list(self._models.items())
for udid, model in items:
proc = self._iproxy_process.get(udid)
# 1) 进程不存在或已退出:直接重启
if proc is None or proc.poll() is not None:
msg = f"[iproxy] 进程已退出,准备重启 | udid={udid}"
print(msg)
LogManager.warning(msg, "iproxy")
self._iproxy_fail_count[udid] = 0
try:
self._start_iproxy(udid, model.screenPort)
except Exception as e:
msg = f"[iproxy] 重启失败 | udid={udid} | err={e}"
print(msg)
LogManager.warning(msg, "iproxy")
continue
# 2) 进程还在,做一次 HTTP 探活
is_ok = self._is_iproxy_http_healthy(model.screenPort)
if is_ok:
if self._iproxy_fail_count.get(udid):
msg = f"[iproxy] HTTP 探活恢复正常 | udid={udid}"
print(msg)
LogManager.info(msg, "iproxy")
self._iproxy_fail_count[udid] = 0
continue
# 3) HTTP 探活失败:记录一次失败
fail = self._iproxy_fail_count.get(udid, 0) + 1
self._iproxy_fail_count[udid] = fail
msg = f"[iproxy] HTTP 探活失败 {fail} 次 | udid={udid}"
print(msg)
LogManager.warning(msg, "iproxy")
FAIL_THRESHOLD = 3
if fail >= FAIL_THRESHOLD:
msg = f"[iproxy] 连续 {fail} 次 HTTP 探活失败,准备重启 | udid={udid}"
print(msg)
LogManager.warning(msg, "iproxy")
self._iproxy_fail_count[udid] = 0
try:
self._stop_iproxy(udid)
self._start_iproxy(udid, model.screenPort)
except Exception as e:
msg = f"[iproxy] HTTP 探活重启失败 | udid={udid} | err={e}"
print(msg)
LogManager.warning(msg, "iproxy")
# ==========================
# 移除设备
# ==========================
def _remove_device(self, udid: str):
print(f"[Remove] 移除设备 {udid}")
# 先停 iproxy
self._stop_iproxy(udid)
with self._lock:
self._models.pop(udid, None)
self._last_seen.pop(udid, None)
self._iproxy_fail_count.pop(udid, None)
self._manager_send()