Files
iOSAI/Module/DeviceInfo.py

800 lines
33 KiB
Python
Raw Normal View History

2025-10-24 14:36:00 +08:00
# -*- coding: utf-8 -*-
2025-10-25 00:22:16 +08:00
"""
极简稳定版设备监督器DeviceInfo加详细 print 日志
- 每个关键节点都会 print便于人工观察执行到哪一步
- 保留核心逻辑监听上下线 / 启动 WDA / iproxy / 通知前端
2025-11-05 17:07:51 +08:00
- 并发提速_add_device 异步化受控并发
- iproxy 守护本地端口 + /status 探活不通则自愈重启连续失败达阈值才移除
2025-10-25 00:22:16 +08:00
"""
import datetime
2025-08-15 20:04:59 +08:00
import os
2025-08-01 13:43:51 +08:00
import time
2025-10-25 00:22:16 +08:00
import threading
import subprocess
2025-10-24 14:36:00 +08:00
import socket
from concurrent.futures import ThreadPoolExecutor
2025-10-25 00:22:16 +08:00
from pathlib import Path
2025-10-31 13:19:55 +08:00
from typing import Dict, Optional, List, Any
2025-10-24 16:24:09 +08:00
import platform
2025-10-25 00:22:16 +08:00
import psutil
import http.client
2025-09-24 16:32:05 +08:00
import tidevice
2025-09-18 21:31:23 +08:00
import wda
2025-09-04 20:47:14 +08:00
from tidevice import Usbmux, ConnectionType
2025-09-08 13:48:21 +08:00
from tidevice._device import BaseDevice
2025-10-24 14:36:00 +08:00
2025-08-01 13:43:51 +08:00
from Entity.DeviceModel import DeviceModel
2025-10-25 00:22:16 +08:00
from Entity.Variables import WdaAppBundleId, wdaScreenPort, wdaFunctionPort
2025-08-01 13:43:51 +08:00
from Module.FlaskSubprocessManager import FlaskSubprocessManager
2025-10-21 15:43:02 +08:00
from Module.IOSActivator import IOSActivator
from Utils.LogManager import LogManager
2025-08-01 13:43:51 +08:00
2025-09-11 22:46:55 +08:00
2025-10-24 16:24:09 +08:00
def _monotonic() -> float:
return time.monotonic()
2025-10-25 00:22:16 +08:00
def _is_port_free(port: int, host: str = "127.0.0.1") -> bool:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port))
return True
except OSError:
return False
finally:
s.close()
def _pick_free_port(low: int = 20000, high: int = 48000) -> int:
"""全局兜底的端口选择:先随机后顺扫,避免固定起点导致碰撞。支持通过环境变量覆盖范围:
PORT_RANGE_LOW / PORT_RANGE_HIGH
"""
try:
low = int(os.getenv("PORT_RANGE_LOW", str(low)))
high = int(os.getenv("PORT_RANGE_HIGH", str(high)))
except Exception:
pass
if high - low < 100:
high = low + 100
import random
# 随机尝试 64 次
tried = set()
for _ in range(64):
p = random.randint(low, high)
if p in tried:
continue
tried.add(p)
if _is_port_free(p):
return p
# 顺序兜底
for p in range(low, high):
if p in tried:
continue
if _is_port_free(p):
return p
raise RuntimeError("未找到可用端口")
2025-10-24 16:24:09 +08:00
2025-10-24 22:04:28 +08:00
2025-10-25 00:22:16 +08:00
class DeviceInfo:
2025-11-03 14:27:31 +08:00
_instance = None
_instance_lock = threading.Lock()
def __new__(cls, *args, **kwargs):
# 双重检查锁,确保线程安全单例
if not cls._instance:
with cls._instance_lock:
if not cls._instance:
cls._instance = super().__new__(cls)
return cls._instance
2025-10-25 00:22:16 +08:00
# ---- 端口分配:加一个最小的“保留池”,避免并发选到同一个端口 ----
def _alloc_port(self) -> int:
with self._lock:
busy = set(self._port_by_udid.values()) | set(self._reserved_ports)
# 优先随机尝试若干次,减少并发碰撞
import random
low = int(os.getenv("PORT_RANGE_LOW", "20000"))
high = int(os.getenv("PORT_RANGE_HIGH", "48000"))
for _ in range(128):
p = random.randint(low, high)
with self._lock:
if p not in busy and p not in self._reserved_ports and _is_port_free(p):
self._reserved_ports.add(p)
return p
# 兜底顺序扫描
for p in range(low, high):
with self._lock:
if p in self._reserved_ports or p in busy:
continue
if _is_port_free(p):
with self._lock:
self._reserved_ports.add(p)
return p
raise RuntimeError("端口分配失败:没有可用端口")
2025-10-24 22:04:28 +08:00
2025-10-25 00:22:16 +08:00
def _release_port(self, port: int):
with self._lock:
self._reserved_ports.discard(port)
2025-09-22 14:36:05 +08:00
2025-10-25 00:22:16 +08:00
ADD_STABLE_SEC = float(os.getenv("ADD_STABLE_SEC", "2.0"))
REMOVE_GRACE_SEC = float(os.getenv("REMOVE_GRACE_SEC", "6.0"))
WDA_READY_TIMEOUT = float(os.getenv("WDA_READY_TIMEOUT", "35.0"))
2025-09-28 14:35:09 +08:00
2025-10-25 00:22:16 +08:00
def __init__(self) -> None:
2025-11-03 14:27:31 +08:00
# 防止多次初始化(因为 __init__ 每次调用 DeviceInfo() 都会执行)
if getattr(self, "_initialized", False):
return
2025-10-23 21:38:18 +08:00
self._lock = threading.RLock()
2025-10-25 00:22:16 +08:00
self._models: Dict[str, DeviceModel] = {}
self._iproxy: Dict[str, subprocess.Popen] = {}
self._port_by_udid: Dict[str, int] = {}
self._reserved_ports: set[int] = set()
self._first_seen: Dict[str, float] = {}
self._last_seen: Dict[str, float] = {}
self._manager = FlaskSubprocessManager.get_instance()
self._iproxy_path = self._find_iproxy()
2025-11-05 17:07:51 +08:00
# 懒加载线程池属性(供 _add_device 并发使用)
self._add_lock: Optional[threading.RLock] = None
self._adding_udids: Optional[set[str]] = None
self._add_executor: Optional["ThreadPoolExecutor"] = None
2025-11-05 17:07:51 +08:00
# iproxy 连续失败计数(守护用)
self._iproxy_fail_count: Dict[str, int] = {}
2025-10-25 00:22:16 +08:00
LogManager.info("DeviceInfo 初始化完成", udid="system")
print("[Init] DeviceInfo 初始化完成")
2025-10-30 20:11:14 +08:00
2025-11-05 17:07:51 +08:00
# iproxy 守护线程(端口+HTTP探活 → 自愈重启 → 达阈值才移除)
threading.Thread(target=self.check_iproxy_ports, daemon=True).start()
self._initialized = True # 标记已初始化
# =============== 并发添加设备:最小改动(包装 _add_device ===============
def _ensure_add_executor(self):
"""
懒加载首次调用 _add_device 时初始化线程池与去重集合
注意不要只用 hasattr属性可能已在 __init__ 里置为 None
2025-11-05 17:07:51 +08:00
"""
# 1) 锁
if getattr(self, "_add_lock", None) is None:
2025-11-05 17:07:51 +08:00
self._add_lock = threading.RLock()
# 2) 去重集合
if getattr(self, "_adding_udids", None) is None:
2025-11-05 17:07:51 +08:00
self._adding_udids = set()
# 3) 线程池
if getattr(self, "_add_executor", None) is None:
2025-11-05 17:07:51 +08:00
from concurrent.futures import ThreadPoolExecutor
import os
2025-11-05 17:07:51 +08:00
max_workers = max(2, min(6, (os.cpu_count() or 4) // 2))
self._add_executor = ThreadPoolExecutor(
max_workers=max_workers,
thread_name_prefix="dev-add"
)
try:
LogManager.info(f"[Init] Device add executor started, max_workers={max_workers}", udid="system")
except Exception:
pass
def _safe_add_device(self, udid: str):
"""
后台执行真正的新增实现_add_device_impl
- 任何异常只记日志不抛出
- 无论成功与否都在 finally 里清理正在添加标记
"""
try:
self._add_device_impl(udid) # ← 这是你原来的重逻辑(见下方)
except Exception as e:
try:
LogManager.method_error(f"_add_device_impl 异常:{e}", "_safe_add_device", udid=udid)
except Exception:
pass
finally:
lock = getattr(self, "_add_lock", None)
if lock is None:
# 极端容错,避免再次抛异常
self._add_lock = lock = threading.RLock()
with lock:
2025-11-05 17:07:51 +08:00
self._adding_udids.discard(udid)
2025-10-31 13:19:55 +08:00
2025-11-05 17:07:51 +08:00
def _add_device(self, udid: str):
"""并发包装器:保持所有调用点不变。"""
2025-11-05 17:07:51 +08:00
self._ensure_add_executor()
# 保险:即使极端情况下属性仍是 None也就地补齐一次
lock = getattr(self, "_add_lock", None)
if lock is None:
self._add_lock = lock = threading.RLock()
adding = getattr(self, "_adding_udids", None)
if adding is None:
self._adding_udids = adding = set()
# 去重:同一 udid 只提交一次
with lock:
if udid in adding:
2025-11-05 17:07:51 +08:00
return
adding.add(udid)
2025-11-05 17:07:51 +08:00
try:
# 注意submit(fn, udid) —— 这里不是 *args=udid直接传第二个位置参数即可
2025-11-05 17:07:51 +08:00
self._add_executor.submit(self._safe_add_device, udid)
except Exception as e:
# 提交失败要把去重标记清掉
with lock:
adding.discard(udid)
2025-11-05 17:07:51 +08:00
try:
LogManager.method_error(text=f"提交新增任务失败:{e}", method="_add_device", udid=udid)
2025-11-05 17:07:51 +08:00
except Exception:
pass
# =============== iproxy 健康检查 / 自愈 ===============
def _iproxy_tcp_probe(self, port: int, timeout: float = 0.6) -> bool:
"""快速 TCP 探测:能建立连接即认为本地监听正常。"""
try:
with socket.create_connection(("127.0.0.1", int(port)), timeout=timeout):
return True
except Exception:
2025-10-31 13:19:55 +08:00
return False
2025-11-05 17:07:51 +08:00
def _iproxy_http_status_ok_quick(self, port: int, timeout: float = 1.2) -> bool:
"""
轻量 HTTP 探测GET /status
- 成功返回 2xx/3xx 视为 OK
- 4xx/5xx 也说明链路畅通服务可交互这里统一认为 OK避免误判
"""
2025-10-31 13:19:55 +08:00
try:
2025-10-31 19:41:44 +08:00
conn = http.client.HTTPConnection("127.0.0.1", int(port), timeout=timeout)
2025-11-05 17:07:51 +08:00
conn.request("GET", "/status")
2025-10-31 19:41:44 +08:00
resp = conn.getresponse()
2025-11-05 17:07:51 +08:00
_ = resp.read(128)
code = getattr(resp, "status", 0)
2025-10-31 19:41:44 +08:00
conn.close()
2025-11-05 17:07:51 +08:00
# 任何能返回 HTTP 的,都说明“有服务可交互”
return 100 <= code <= 599
except Exception:
return False
def _iproxy_health_ok(self, udid: str, port: int) -> bool:
# 1) 监听检测:不通直接 False
2025-11-05 17:07:51 +08:00
if not self._iproxy_tcp_probe(port, timeout=0.6):
return False
# 2) 业务探测:/status 慢可能是 WDA 卡顿;失败不等同于“端口坏”
2025-11-05 17:07:51 +08:00
if not self._iproxy_http_status_ok_quick(port, timeout=1.2):
print(f"[iproxy-health] /status 超时,视为轻微异常 {udid}:{port}")
return True
2025-11-05 17:07:51 +08:00
return True
2025-10-31 19:41:44 +08:00
2025-11-05 17:07:51 +08:00
def _restart_iproxy(self, udid: str, port: int) -> bool:
"""干净重启 iproxy先杀旧的再启动新的并等待监听。"""
print(f"[iproxy-guard] 准备重启 iproxy {udid} on {port}")
proc = None
with self._lock:
old = self._iproxy.get(udid)
try:
if old:
self._kill(old)
except Exception as e:
print(f"[iproxy-guard] 杀旧进程异常 {udid}: {e}")
# 重新拉起
try:
proc = self._start_iproxy(udid, local_port=port)
2025-10-31 19:41:44 +08:00
except Exception as e:
2025-11-05 17:07:51 +08:00
print(f"[iproxy-guard] 重启失败 {udid}: {e}")
proc = None
if not proc:
2025-10-31 13:19:55 +08:00
return False
2025-11-05 17:07:51 +08:00
# 写回进程表
with self._lock:
self._iproxy[udid] = proc
print(f"[iproxy-guard] 重启成功 {udid} port={port}")
return True
# =============== 一轮检查:先自愈,仍失败才考虑移除 =================
2025-10-31 13:19:55 +08:00
def check_iproxy_ports(self, connect_timeout: float = 3) -> None:
2025-11-05 17:07:51 +08:00
"""
周期性巡检默认每 10s 一次
- 在线设备(type=1)
1) 先做 TCP+HTTP(/status) 探测封装在 _iproxy_health_ok
2) 失败 自愈重启 iproxy仍失败则累计失败计数
3) 连续失败次数 >= 阈值 不删除设备只标记降级ready=False, streamBroken=True
4) 恢复时清零计数并标记恢复ready=True, streamBroken=False
2025-11-05 17:07:51 +08:00
"""
# 启动延迟,等新增流程跑起来,避免误判
2025-10-31 19:41:44 +08:00
time.sleep(20)
2025-11-05 17:07:51 +08:00
FAIL_THRESHOLD = int(os.getenv("IPROXY_FAIL_THRESHOLD", "3")) # 连续失败阈值(可用环境变量调)
INTERVAL_SEC = int(os.getenv("IPROXY_CHECK_INTERVAL", "10")) # 巡检间隔
2025-11-05 17:07:51 +08:00
try:
while True:
snapshot = list(self._models.items()) # [(deviceId, DeviceModel), ...]
for device_id, model in snapshot:
try:
if model.type != 1:
# 离线设备清零计数
self._iproxy_fail_count.pop(device_id, None)
continue
port = int(model.screenPort)
if port <= 0 or port > 65535:
continue
# 健康探测
ok = self._iproxy_health_ok(device_id, port)
if ok:
# 健康:清零计数
if self._iproxy_fail_count.get(device_id):
self._iproxy_fail_count[device_id] = 0
# CHANGED: 若之前降级过,这里标记恢复并上报
need_report = False
with self._lock:
m = self._models.get(device_id)
if m:
prev_ready = getattr(m, "ready", True)
prev_broken = getattr(m, "streamBroken", False)
if (not prev_ready) or prev_broken:
m.ready = True
if prev_broken:
try:
delattr(m, "streamBroken")
except Exception:
setattr(m, "streamBroken", False)
need_report = True
if need_report and m:
try:
print(f"[iproxy-check] 自愈成功,恢复就绪 deviceId={device_id} port={port}")
self._manager_send(m)
except Exception as e:
print(f"[iproxy-check] 上报恢复异常 deviceId={device_id}: {e}")
# print(f"[iproxy-check] OK deviceId={device_id} port={port}")
continue
# 第一次失败:尝试自愈重启
print(f"[iproxy-check] 探活失败,准备自愈重启 deviceId={device_id} port={port}")
healed = self._restart_iproxy(device_id, port)
# 重启后再探测一次
ok2 = self._iproxy_health_ok(device_id, port) if healed else False
if ok2:
print(f"[iproxy-check] 自愈成功 deviceId={device_id} port={port}")
2025-11-05 17:07:51 +08:00
self._iproxy_fail_count[device_id] = 0
# CHANGED: 若之前降级过,这里也顺便恢复并上报
need_report = False
with self._lock:
m = self._models.get(device_id)
if m:
prev_ready = getattr(m, "ready", True)
prev_broken = getattr(m, "streamBroken", False)
if (not prev_ready) or prev_broken:
m.ready = True
if prev_broken:
try:
delattr(m, "streamBroken")
except Exception:
setattr(m, "streamBroken", False)
need_report = True
if need_report and m:
try:
self._manager_send(m)
except Exception as e:
print(f"[iproxy-check] 上报恢复异常 deviceId={device_id}: {e}")
continue
# 自愈失败:累计失败计数
fails = self._iproxy_fail_count.get(device_id, 0) + 1
self._iproxy_fail_count[device_id] = fails
print(f"[iproxy-check] 自愈失败 ×{fails} deviceId={device_id} port={port}")
# 达阈值 → 【不移除设备】,改为降级并上报(避免“删了又加”的抖动)
if fails >= FAIL_THRESHOLD:
with self._lock:
m = self._models.get(device_id)
if m:
m.ready = False
setattr(m, "streamBroken", True)
try:
if m:
print(
f"[iproxy-check] 连续失败 {fails} 次,降级设备(保留在线) deviceId={device_id} port={port}")
self._manager_send(m)
except Exception as e:
print(f"[iproxy-check] 上报降级异常 deviceId={device_id}: {e}")
2025-10-31 13:19:55 +08:00
except Exception as e:
print(f"[iproxy-check] 单设备检查异常: {e}")
2025-11-05 17:07:51 +08:00
time.sleep(INTERVAL_SEC)
except Exception as e:
print("检查iproxy状态遇到错误",e)
LogManager.error("检查iproxy状态遇到错误",e)
2025-10-23 21:38:18 +08:00
2025-09-22 14:36:05 +08:00
def listen(self):
2025-10-25 00:22:16 +08:00
LogManager.method_info("进入主循环", "listen", udid="system")
print("[Listen] 开始监听设备上下线...")
2025-09-15 22:40:45 +08:00
while True:
2025-10-23 21:38:18 +08:00
try:
usb = Usbmux().device_list()
2025-10-25 00:22:16 +08:00
online = {d.udid for d in usb if d.conn_type == ConnectionType.USB}
2025-10-23 21:38:18 +08:00
except Exception as e:
2025-10-24 14:36:00 +08:00
LogManager.warning(f"[device_list] 异常:{e}", udid="system")
2025-10-25 00:22:16 +08:00
print(f"[Listen] 获取设备列表异常: {e}")
2025-10-23 21:38:18 +08:00
time.sleep(1)
continue
2025-09-28 14:35:09 +08:00
2025-10-25 00:22:16 +08:00
now = _monotonic()
for u in online:
self._first_seen.setdefault(u, now)
2025-10-23 21:38:18 +08:00
self._last_seen[u] = now
with self._lock:
known = set(self._models.keys())
2025-10-24 14:36:00 +08:00
2025-10-25 00:22:16 +08:00
for udid in online - known:
if (now - self._first_seen.get(udid, now)) >= self.ADD_STABLE_SEC:
print(datetime.datetime.now().strftime("%H:%M:%S"))
2025-10-25 00:22:16 +08:00
print(f"[Add] 检测到新设备: {udid}")
try:
2025-11-05 17:07:51 +08:00
self._add_device(udid) # ← 并发包装器
2025-10-25 00:22:16 +08:00
except Exception as e:
LogManager.method_error(f"新增失败:{e}", "listen", udid=udid)
print(f"[Add] 新增失败 {udid}: {e}")
2025-10-23 21:38:18 +08:00
for udid in list(known):
2025-10-25 00:22:16 +08:00
if udid in online:
2025-10-24 22:04:28 +08:00
continue
2025-10-23 21:38:18 +08:00
last = self._last_seen.get(udid, 0.0)
2025-10-25 00:22:16 +08:00
if (now - last) >= self.REMOVE_GRACE_SEC:
print(f"[Remove] 检测到设备离线: {udid}")
2025-10-24 22:04:28 +08:00
try:
2025-10-25 00:22:16 +08:00
self._remove_device(udid)
2025-10-24 22:04:28 +08:00
except Exception as e:
2025-10-25 00:22:16 +08:00
LogManager.method_error(f"移除失败:{e}", "listen", udid=udid)
print(f"[Remove] 移除失败 {udid}: {e}")
2025-09-28 14:35:09 +08:00
2025-09-15 22:40:45 +08:00
time.sleep(1)
2025-10-27 21:44:16 +08:00
def _wait_wda_ready_on_port(self, udid: str, local_port: int, total_timeout_sec: float = None) -> bool:
"""在给定的本地映射端口上等待 /status 就绪。"""
import http.client, time
if total_timeout_sec is None:
total_timeout_sec = self.WDA_READY_TIMEOUT
deadline = _monotonic() + total_timeout_sec
attempt = 0
while _monotonic() < deadline:
attempt += 1
try:
conn = http.client.HTTPConnection("127.0.0.1", local_port, timeout=1.8)
conn.request("GET", "/status")
resp = conn.getresponse()
_ = resp.read(128)
code = getattr(resp, "status", 0)
ok = 200 <= code < 400
print(f"[WDA] /status@{local_port}{attempt}次 code={code}, ok={ok} {udid}")
if ok:
return True
except Exception as e:
print(f"[WDA] /status@{local_port} 异常({attempt}): {e}")
time.sleep(0.5)
print(f"[WDA] /status@{local_port} 等待超时 {udid}")
return False
2025-11-05 17:07:51 +08:00
# ---------------- 原 _add_device 实现:整体改名为 _add_device_impl ----------------
def _add_device_impl(self, udid: str):
2025-10-25 00:22:16 +08:00
print(f"[Add] 开始新增设备 {udid}")
2025-10-24 14:36:00 +08:00
2025-09-22 14:36:05 +08:00
if not self._trusted(udid):
2025-10-25 00:22:16 +08:00
print(f"[Add] 未信任设备 {udid}, 跳过")
2025-09-22 14:36:05 +08:00
return
2025-10-24 14:36:00 +08:00
2025-11-05 17:07:51 +08:00
# 先分配一个“正式使用”的本地端口,并立即起 iproxy只起这一回
port = self._alloc_port()
print(f"[iproxy] 准备启动 iproxy 映射 {port}->{wdaScreenPort} (正式)")
proc = self._start_iproxy(udid, local_port=port)
if not proc:
self._release_port(port)
print(f"[iproxy] 启动失败,放弃新增 {udid}")
return
# 判断 WDA 是否已就绪;如果未就绪,按原逻辑拉起 WDA 并等到就绪
2025-10-24 16:24:09 +08:00
try:
dev = tidevice.Device(udid)
2025-10-25 00:22:16 +08:00
major = int(dev.product_version.split(".")[0])
except Exception:
major = 0
2025-10-24 14:36:00 +08:00
2025-11-05 17:07:51 +08:00
# 直接用“正式端口”探测 /status避免再启一次临时 iproxy
if not self._wait_wda_ready_on_port(udid, local_port=port, total_timeout_sec=3.0):
# 如果还没起来,按你原逻辑拉起 WDA 再等
2025-10-25 00:22:16 +08:00
if major > 17:
2025-10-27 21:44:16 +08:00
print("进入iOS17设备的分支")
2025-11-05 17:07:51 +08:00
try:
IOSActivator().activate(udid)
print("wda启动完成")
except Exception as e:
print(f"[WDA] iOS17 激活异常: {e}")
2025-10-24 16:24:09 +08:00
else:
2025-10-25 00:22:16 +08:00
print(f"[WDA] iOS<=17 启动 WDA app_start (port={wdaScreenPort})")
2025-11-05 17:07:51 +08:00
try:
dev = tidevice.Device(udid)
dev.app_start(WdaAppBundleId)
time.sleep(2)
except Exception as e:
print(f"[WDA] app_start 异常: {e}")
if not self._wait_wda_ready_on_port(udid, local_port=port, total_timeout_sec=self.WDA_READY_TIMEOUT):
2025-10-25 00:22:16 +08:00
print(f"[WDA] WDA 未在超时内就绪, 放弃新增 {udid}")
2025-11-05 17:07:51 +08:00
# 清理已起的正式 iproxy
try:
self._kill(proc)
except Exception:
pass
self._release_port(port)
2025-10-24 16:24:09 +08:00
return
2025-10-24 14:36:00 +08:00
2025-10-25 00:22:16 +08:00
print(f"[WDA] WDA 就绪,准备获取屏幕信息 {udid}")
time.sleep(0.5)
2025-11-05 17:07:51 +08:00
# 带超时的屏幕信息获取(保留你原有容错/重试)
2025-10-25 00:22:16 +08:00
w, h, s = self._screen_info_with_timeout(udid, timeout=3.5)
if not (w and h and s):
for i in range(4):
2025-10-27 21:44:16 +08:00
print(f"[Screen] 第{i + 1}次获取失败, 重试中... {udid}")
2025-10-25 00:22:16 +08:00
time.sleep(0.6)
w, h, s = self._screen_info_with_timeout(udid, timeout=3.5)
if w and h and s:
break
if not (w and h and s):
print(f"[Screen] 屏幕信息仍为空,继续添加 {udid}")
2025-11-05 17:07:51 +08:00
# 写入模型 & 发送前端
2025-10-23 21:38:18 +08:00
with self._lock:
2025-10-24 14:36:00 +08:00
model = DeviceModel(deviceId=udid, screenPort=port, width=w, height=h, scale=s, type=1)
2025-10-23 21:38:18 +08:00
model.ready = True
self._models[udid] = model
2025-10-25 00:22:16 +08:00
self._iproxy[udid] = proc
self._port_by_udid[udid] = port
2025-11-05 17:07:51 +08:00
if hasattr(self, "_iproxy_fail_count"):
self._iproxy_fail_count[udid] = 0
2025-10-21 16:55:40 +08:00
2025-10-25 00:22:16 +08:00
print(f"[Manager] 准备发送设备数据到前端 {udid}")
2025-09-22 14:36:05 +08:00
self._manager_send(model)
print(datetime.datetime.now().strftime("%H:%M:%S"))
2025-10-25 00:22:16 +08:00
print(f"[Add] 设备添加成功 {udid}, port={port}, {w}x{h}@{s}")
2025-09-22 14:36:05 +08:00
def _remove_device(self, udid: str):
2025-10-31 13:19:55 +08:00
"""
移除设备及其转发通知上层
幂等重复调用不会出错
"""
2025-10-25 00:22:16 +08:00
print(f"[Remove] 正在移除设备 {udid}")
2025-10-31 13:19:55 +08:00
# --- 1. 锁内执行所有轻量字典操作 ---
2025-10-23 21:38:18 +08:00
with self._lock:
model = self._models.pop(udid, None)
2025-10-25 00:22:16 +08:00
proc = self._iproxy.pop(udid, None)
2025-10-24 14:36:00 +08:00
self._port_by_udid.pop(udid, None)
2025-10-24 16:24:09 +08:00
self._first_seen.pop(udid, None)
2025-10-25 00:22:16 +08:00
self._last_seen.pop(udid, None)
2025-11-05 17:07:51 +08:00
self._iproxy_fail_count.pop(udid, None)
2025-10-24 14:36:00 +08:00
2025-10-31 13:19:55 +08:00
# --- 2. 锁外执行重操作 ---
# 杀进程
try:
self._kill(proc)
except Exception as e:
print(f"[Remove] 杀进程异常 {udid}: {e}")
2025-10-24 16:24:09 +08:00
2025-10-31 13:19:55 +08:00
# 准备下线模型model 可能为 None
2025-10-24 16:24:09 +08:00
if model is None:
2025-10-31 13:19:55 +08:00
model = DeviceModel(
deviceId=udid, screenPort=-1, width=0, height=0, scale=0.0, type=2
)
# 标记状态为离线
2025-10-24 16:24:09 +08:00
model.type = 2
model.ready = False
model.screenPort = -1
2025-10-31 13:19:55 +08:00
# 通知上层
try:
self._manager_send(model)
except Exception as e:
print(f"[Remove] 通知上层异常 {udid}: {e}")
2025-10-25 00:22:16 +08:00
print(f"[Remove] 设备移除完成 {udid}")
2025-09-22 14:36:05 +08:00
def _trusted(self, udid: str) -> bool:
try:
2025-09-22 14:36:05 +08:00
BaseDevice(udid).get_value("DeviceName")
2025-10-25 00:22:16 +08:00
print(f"[Trust] 设备 {udid} 已信任")
2025-09-22 14:36:05 +08:00
return True
except Exception:
2025-10-25 00:22:16 +08:00
print(f"[Trust] 设备 {udid} 未信任")
2025-09-22 14:36:05 +08:00
return False
2025-09-17 22:23:57 +08:00
2025-10-25 00:22:16 +08:00
def _wda_http_status_ok_once(self, udid: str, timeout_sec: float = 1.8) -> bool:
2025-10-27 21:44:16 +08:00
"""只做一次 /status 探测。任何异常都返回 False不让外层炸掉。"""
tmp_port = None
2025-10-25 00:22:16 +08:00
proc = None
2025-09-24 16:32:05 +08:00
try:
2025-10-27 21:44:16 +08:00
tmp_port = self._alloc_port() # 这里可能抛异常
2025-10-25 00:22:16 +08:00
print(f"[WDA] 启动临时 iproxy 以检测 /status {udid}")
proc = self._spawn_iproxy(udid, local_port=tmp_port, remote_port=wdaScreenPort)
2025-10-27 21:44:16 +08:00
if not proc:
print("[WDA] 启动临时 iproxy 失败")
return False
2025-10-25 00:22:16 +08:00
if not self._wait_until_listening(tmp_port, 3.0):
print(f"[WDA] 临时端口未监听 {tmp_port}")
2025-10-24 16:24:09 +08:00
return False
2025-10-27 21:44:16 +08:00
# 最多两次快速探测
2025-10-25 00:22:16 +08:00
for i in (1, 2):
try:
2025-10-27 21:44:16 +08:00
import http.client
2025-10-25 00:22:16 +08:00
conn = http.client.HTTPConnection("127.0.0.1", tmp_port, timeout=timeout_sec)
conn.request("GET", "/status")
resp = conn.getresponse()
_ = resp.read(128)
code = getattr(resp, "status", 0)
ok = 200 <= code < 400
print(f"[WDA] /status 第{i}次 code={code}, ok={ok}")
if ok:
return True
except Exception as e:
print(f"[WDA] /status 异常({i}): {e}")
2025-10-27 21:44:16 +08:00
time.sleep(0.25)
return False
except Exception as e:
import traceback
print(f"[WDA][probe] 异常:{e}\n{traceback.format_exc()}")
2025-10-25 00:22:16 +08:00
return False
2025-10-27 21:44:16 +08:00
2025-10-24 22:04:28 +08:00
finally:
2025-10-25 00:22:16 +08:00
if proc:
self._kill(proc)
2025-10-27 21:44:16 +08:00
if tmp_port is not None:
self._release_port(tmp_port)
2025-10-25 00:22:16 +08:00
def _wait_wda_ready_http(self, udid: str, total_timeout_sec: float) -> bool:
print(f"[WDA] 等待 WDA Ready (超时 {total_timeout_sec}s) {udid}")
2025-10-24 16:24:09 +08:00
deadline = _monotonic() + total_timeout_sec
while _monotonic() < deadline:
2025-10-25 00:22:16 +08:00
if self._wda_http_status_ok_once(udid):
print(f"[WDA] WDA 就绪 {udid}")
2025-10-24 16:24:09 +08:00
return True
2025-10-25 00:22:16 +08:00
time.sleep(0.6)
print(f"[WDA] WDA 等待超时 {udid}")
2025-10-24 16:24:09 +08:00
return False
2025-09-22 14:36:05 +08:00
def _screen_info(self, udid: str):
2025-08-15 20:04:59 +08:00
try:
2025-10-25 00:22:16 +08:00
# 避免 c.home() 可能触发的阻塞,直接取 window_size
2025-10-21 15:43:02 +08:00
c = wda.USBClient(udid, wdaFunctionPort)
2025-09-28 14:35:09 +08:00
size = c.window_size()
2025-10-25 00:22:16 +08:00
print(f"[Screen] 成功获取屏幕 {int(size.width)}x{int(size.height)} {udid}")
return int(size.width), int(size.height), float(c.scale)
2025-09-24 16:32:05 +08:00
except Exception as e:
2025-10-25 00:22:16 +08:00
print(f"[Screen] 获取屏幕信息异常: {e} {udid}")
return 0, 0, 0.0
2025-09-17 22:23:57 +08:00
2025-10-25 00:22:16 +08:00
def _screen_info_with_timeout(self, udid: str, timeout: float = 3.5):
"""在线程里调用 _screen_info超时返回 0 值,防止卡死。"""
import threading
result = {"val": (0, 0, 0.0)}
done = threading.Event()
def _target():
try:
result["val"] = self._screen_info(udid)
finally:
done.set()
t = threading.Thread(target=_target, daemon=True)
t.start()
if not done.wait(timeout):
print(f"[Screen] 获取屏幕信息超时({timeout}s) {udid}")
return 0, 0, 0.0
return result["val"]
def _wait_until_listening(self, port: int, timeout: float) -> bool:
for to in (1.5, 2.5, 3.5):
2025-10-24 16:24:09 +08:00
deadline = _monotonic() + to
while _monotonic() < deadline:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
2025-10-24 22:04:28 +08:00
s.settimeout(0.25)
2025-10-24 16:24:09 +08:00
if s.connect_ex(("127.0.0.1", port)) == 0:
2025-10-25 00:22:16 +08:00
print(f"[Port] 端口 {port} 已监听")
2025-10-24 16:24:09 +08:00
return True
2025-10-25 00:22:16 +08:00
time.sleep(0.05)
print(f"[Port] 端口 {port} 未监听")
2025-10-24 14:36:00 +08:00
return False
2025-10-25 00:22:16 +08:00
def _spawn_iproxy(self, udid: str, local_port: int, remote_port: int) -> Optional[subprocess.Popen]:
creationflags = 0
startupinfo = None
if os.name == "nt":
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000) | \
getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0x00000200)
si = subprocess.STARTUPINFO()
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
si.wShowWindow = 0
startupinfo = si
cmd = [self._iproxy_path, "-u", udid, str(local_port), str(remote_port)]
2025-08-15 20:04:59 +08:00
try:
2025-10-25 00:22:16 +08:00
print(f"[iproxy] 启动进程 {cmd}")
return subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
creationflags=creationflags,
startupinfo=startupinfo,
)
except Exception as e:
print(f"[iproxy] 创建进程失败: {e}")
2025-10-24 14:36:00 +08:00
return None
2025-10-25 00:22:16 +08:00
def _start_iproxy(self, udid: str, local_port: int) -> Optional[subprocess.Popen]:
proc = self._spawn_iproxy(udid, local_port=local_port, remote_port=wdaScreenPort)
if not proc:
print(f"[iproxy] 启动失败 {udid}")
return None
if not self._wait_until_listening(local_port, 3.0):
self._kill(proc)
print(f"[iproxy] 未监听, 已杀死 {udid}")
2025-09-22 14:36:05 +08:00
return None
2025-10-25 00:22:16 +08:00
print(f"[iproxy] 启动成功 port={local_port} {udid}")
return proc
2025-09-17 22:23:57 +08:00
2025-09-22 14:36:05 +08:00
def _kill(self, proc: Optional[subprocess.Popen]):
if not proc:
2025-09-20 20:07:16 +08:00
return
2025-09-11 22:46:55 +08:00
try:
2025-10-24 16:24:09 +08:00
p = psutil.Process(proc.pid)
p.terminate()
2025-09-22 14:36:05 +08:00
try:
2025-10-25 00:22:16 +08:00
p.wait(timeout=1.5)
2025-10-24 16:24:09 +08:00
except psutil.TimeoutExpired:
2025-10-25 00:22:16 +08:00
p.kill(); p.wait(timeout=1.5)
print(f"[Proc] 已结束进程 PID={proc.pid}")
2025-10-24 14:36:00 +08:00
except Exception as e:
2025-10-25 00:22:16 +08:00
print(f"[Proc] 结束进程异常: {e}")
2025-09-28 14:35:09 +08:00
2025-10-24 14:36:00 +08:00
def _manager_send(self, model: DeviceModel):
2025-09-28 14:35:09 +08:00
try:
2025-11-05 17:07:51 +08:00
if self._manager.send(model.toDict()):
print(f"[Manager] 已发送前端数据 {model.deviceId}")
return
except Exception as e:
print(f"[Manager] 首次发送异常: {e}")
# 自愈:拉起一次并重试一次(不要用 and 连接)
2025-11-05 17:07:51 +08:00
try:
self._manager.start() # 不关心返回值
if self._manager.send(model.toDict()):
2025-11-05 17:07:51 +08:00
print(f"[Manager] 重试发送成功 {model.deviceId}")
return
2025-10-24 14:36:00 +08:00
except Exception as e:
2025-11-05 17:07:51 +08:00
print(f"[Manager] 重试发送异常: {e}")
2025-10-23 21:38:18 +08:00
2025-10-24 14:36:00 +08:00
def _find_iproxy(self) -> str:
2025-10-24 16:24:09 +08:00
env_path = os.getenv("IPROXY_PATH")
if env_path and Path(env_path).is_file():
2025-10-25 00:22:16 +08:00
print(f"[iproxy] 使用环境变量路径 {env_path}")
2025-10-24 16:24:09 +08:00
return env_path
2025-10-24 14:36:00 +08:00
base = Path(__file__).resolve().parent.parent
2025-10-25 00:22:16 +08:00
name = "iproxy.exe" if os.name == "nt" else "iproxy"
2025-10-24 14:36:00 +08:00
path = base / "resources" / "iproxy" / name
if path.is_file():
2025-10-25 00:22:16 +08:00
print(f"[iproxy] 使用默认路径 {path}")
2025-10-24 14:36:00 +08:00
return str(path)
2025-11-05 17:07:51 +08:00
raise FileNotFoundError(f"iproxy 不存在: {path}")