2025-11-20 16:49:37 +08:00
|
|
|
|
|
2025-11-19 17:23:41 +08:00
|
|
|
|
import http.client
|
|
|
|
|
|
import json
|
2025-08-15 20:04:59 +08:00
|
|
|
|
import os
|
2025-11-19 17:23:41 +08:00
|
|
|
|
import socket
|
|
|
|
|
|
import subprocess
|
2025-11-13 19:28:57 +08:00
|
|
|
|
import sys
|
2025-10-25 00:22:16 +08:00
|
|
|
|
import threading
|
2025-11-19 17:23:41 +08:00
|
|
|
|
import time
|
2025-11-05 21:04:04 +08:00
|
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
2025-10-25 00:22:16 +08:00
|
|
|
|
from pathlib import Path
|
2025-11-19 17:23:41 +08:00
|
|
|
|
from typing import Dict, Optional
|
2025-10-25 00:22:16 +08:00
|
|
|
|
import psutil
|
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-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
|
2025-08-07 20:55:30 +08:00
|
|
|
|
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"))
|
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
|
|
|
|
|
2025-11-05 21:04:04 +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 时初始化线程池与去重集合。
|
2025-11-05 21:04:04 +08:00
|
|
|
|
注意:不要只用 hasattr;属性可能已在 __init__ 里置为 None。
|
2025-11-05 17:07:51 +08:00
|
|
|
|
"""
|
2025-11-05 21:04:04 +08:00
|
|
|
|
# 1) 锁
|
|
|
|
|
|
if getattr(self, "_add_lock", None) is None:
|
2025-11-05 17:07:51 +08:00
|
|
|
|
self._add_lock = threading.RLock()
|
2025-11-05 21:04:04 +08:00
|
|
|
|
|
|
|
|
|
|
# 2) 去重集合
|
|
|
|
|
|
if getattr(self, "_adding_udids", None) is None:
|
2025-11-05 17:07:51 +08:00
|
|
|
|
self._adding_udids = set()
|
2025-11-05 21:04:04 +08:00
|
|
|
|
|
|
|
|
|
|
# 3) 线程池
|
|
|
|
|
|
if getattr(self, "_add_executor", None) is None:
|
2025-11-05 17:07:51 +08:00
|
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
2025-11-05 21:04:04 +08:00
|
|
|
|
import os
|
2025-11-20 16:49:37 +08:00
|
|
|
|
max_workers = 6
|
2025-11-05 17:07:51 +08:00
|
|
|
|
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:
|
2025-11-05 21:04:04 +08:00
|
|
|
|
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 21:04:04 +08:00
|
|
|
|
"""并发包装器:保持所有调用点不变。"""
|
2025-11-05 17:07:51 +08:00
|
|
|
|
self._ensure_add_executor()
|
2025-11-05 21:04:04 +08:00
|
|
|
|
|
|
|
|
|
|
# 保险:即使极端情况下属性仍是 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
|
2025-11-05 21:04:04 +08:00
|
|
|
|
adding.add(udid)
|
|
|
|
|
|
|
2025-11-05 17:07:51 +08:00
|
|
|
|
try:
|
2025-11-05 21:04:04 +08:00
|
|
|
|
# 注意: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:
|
2025-11-05 21:04:04 +08:00
|
|
|
|
# 提交失败要把去重标记清掉
|
|
|
|
|
|
with lock:
|
|
|
|
|
|
adding.discard(udid)
|
2025-11-05 17:07:51 +08:00
|
|
|
|
try:
|
2025-11-05 21:04:04 +08:00
|
|
|
|
LogManager.method_error(text=f"提交新增任务失败:{e}", method="_add_device", udid=udid)
|
2025-11-05 17:07:51 +08:00
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2025-11-20 16:49:37 +08:00
|
|
|
|
def _iproxy_health_ok(self, port: int) -> bool:
|
2025-10-31 13:19:55 +08:00
|
|
|
|
try:
|
2025-11-20 16:49:37 +08:00
|
|
|
|
conn = http.client.HTTPConnection("127.0.0.1", int(port), timeout=1.5)
|
|
|
|
|
|
conn.request("GET", "/")
|
2025-10-31 19:41:44 +08:00
|
|
|
|
resp = conn.getresponse()
|
2025-11-05 17:07:51 +08:00
|
|
|
|
|
2025-11-20 16:49:37 +08:00
|
|
|
|
status = getattr(resp, "status", 0)
|
|
|
|
|
|
ctype = resp.getheader("Content-Type", "") or ""
|
|
|
|
|
|
|
2025-11-19 20:47:28 +08:00
|
|
|
|
conn.close()
|
|
|
|
|
|
|
2025-11-20 16:49:37 +08:00
|
|
|
|
ok = (200 <= status < 400) and ("multipart" in ctype.lower())
|
|
|
|
|
|
|
|
|
|
|
|
if not ok:
|
|
|
|
|
|
LogManager.error(
|
|
|
|
|
|
f"[iproxy] 健康检查失败: status={status}, ctype={ctype!r}, port={port}"
|
|
|
|
|
|
)
|
|
|
|
|
|
return ok
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
LogManager.error(f"[iproxy] 健康检查异常 port={port}: {e}")
|
2025-11-19 20:47:28 +08:00
|
|
|
|
return False
|
|
|
|
|
|
|
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-11-19 20:47:28 +08:00
|
|
|
|
def _restart_wda(self, udid: str) -> bool:
|
2025-11-05 17:07:51 +08:00
|
|
|
|
"""
|
2025-11-19 20:47:28 +08:00
|
|
|
|
重启指定设备上的 WDA(用于已在系统中“在线”的设备):
|
|
|
|
|
|
- 假定该设备已经完成过信任/配对,不再重复配对
|
|
|
|
|
|
- iOS 17+:直接调用 IOSActivator().activate(udid)
|
|
|
|
|
|
- iOS <=16:走 tidevice.app_start(WdaAppBundleId)
|
|
|
|
|
|
如果当前已存在映射端口,则在该端口上等待 WDA /status 就绪。
|
2025-11-05 17:07:51 +08:00
|
|
|
|
"""
|
2025-11-19 20:47:28 +08:00
|
|
|
|
print(f"[WDA-guard] 尝试重启 WDA: {udid}")
|
2025-11-05 17:07:51 +08:00
|
|
|
|
|
2025-11-12 13:21:36 +08:00
|
|
|
|
try:
|
2025-11-19 20:47:28 +08:00
|
|
|
|
dev = tidevice.Device(udid)
|
|
|
|
|
|
try:
|
|
|
|
|
|
major = int(dev.product_version.split(".")[0])
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
major = 0
|
|
|
|
|
|
print(f"[WDA-guard] 设备 {udid} iOS 主版本号 = {major}")
|
|
|
|
|
|
|
|
|
|
|
|
if major >= 17:
|
|
|
|
|
|
# -------- iOS 17+:不再重复配对,直接激活 --------
|
|
|
|
|
|
print(f"[WDA-guard] iOS17+ 设备,直接通过 IOSActivator 激活 {udid}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
IOSActivator().activate(udid)
|
|
|
|
|
|
print(f"[WDA-guard] iOS17+ 通过 IOSActivator 激活完成 {udid}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[WDA-guard] iOS17+ 激活 WDA 异常: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
# -------- iOS 16 及以下:直接 app_start WDA --------
|
|
|
|
|
|
print(f"[WDA-guard] iOS<=16 设备,准备通过 tidevice.app_start 启动 WDA {udid}")
|
|
|
|
|
|
# app_stop 失败不致命,做一下容错
|
|
|
|
|
|
try:
|
|
|
|
|
|
dev.app_stop(WdaAppBundleId)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[WDA-guard] app_stop 异常(忽略):{e}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
dev.app_start(WdaAppBundleId)
|
|
|
|
|
|
print(f"[WDA-guard] app_start 已调用 {udid}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[WDA-guard] app_start 异常: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
# -------- 如果这台设备已经有固定的 screenPort,就在该端口上等 WDA Ready --------
|
|
|
|
|
|
port = None
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
port = self._port_by_udid.get(udid)
|
|
|
|
|
|
|
|
|
|
|
|
if port:
|
|
|
|
|
|
print(f"[WDA-guard] 已有现成端口 {port},等待 WDA 在该端口就绪 {udid}")
|
|
|
|
|
|
ok = self._wait_wda_ready_on_port(
|
|
|
|
|
|
udid,
|
2025-11-20 16:49:37 +08:00
|
|
|
|
local_port=wdaFunctionPort,
|
2025-11-19 20:47:28 +08:00
|
|
|
|
)
|
|
|
|
|
|
if not ok:
|
|
|
|
|
|
print(f"[WDA-guard] WDA 在端口 {port} 未在超时内就绪 {udid}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"[WDA-guard] 当前无已记录端口(_port_by_udid 无 {udid}),仅完成 WDA 启动,不做就绪检测")
|
|
|
|
|
|
|
|
|
|
|
|
print(f"[WDA-guard] WDA 重启完成 {udid}")
|
|
|
|
|
|
return True
|
2025-11-12 13:21:36 +08:00
|
|
|
|
|
2025-11-19 20:47:28 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[WDA-guard] 重启 WDA 总体异常: {e}")
|
|
|
|
|
|
return False
|
2025-11-12 13:21:36 +08:00
|
|
|
|
|
2025-11-19 20:47:28 +08:00
|
|
|
|
# =============== 一轮检查:先自愈,仍失败才考虑移除 =================
|
|
|
|
|
|
def check_iproxy_ports(self):
|
2025-11-12 13:21:36 +08:00
|
|
|
|
|
2025-11-20 16:49:37 +08:00
|
|
|
|
print("[Guard] iproxy+WDA 守护线程启动")
|
2025-11-05 21:04:04 +08:00
|
|
|
|
|
2025-11-19 20:47:28 +08:00
|
|
|
|
while True:
|
|
|
|
|
|
try:
|
|
|
|
|
|
with self._lock:
|
2025-11-20 16:49:37 +08:00
|
|
|
|
udids = list(self._models.keys())
|
2025-11-19 20:47:28 +08:00
|
|
|
|
|
2025-11-20 16:49:37 +08:00
|
|
|
|
for udid in udids:
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
port = self._port_by_udid.get(udid)
|
|
|
|
|
|
fail = self._iproxy_fail_count.get(udid, 0)
|
2025-11-19 20:47:28 +08:00
|
|
|
|
|
2025-11-20 16:49:37 +08:00
|
|
|
|
if not port:
|
2025-11-19 20:47:28 +08:00
|
|
|
|
continue
|
|
|
|
|
|
|
2025-11-20 16:49:37 +08:00
|
|
|
|
# ==== 第一层:视频流健康检查 ====
|
|
|
|
|
|
ok_stream = self._iproxy_health_ok(port)
|
2025-11-19 20:47:28 +08:00
|
|
|
|
|
2025-11-20 16:49:37 +08:00
|
|
|
|
if ok_stream:
|
|
|
|
|
|
# 成功 → 清零失败计数
|
2025-11-19 20:47:28 +08:00
|
|
|
|
with self._lock:
|
2025-11-20 16:49:37 +08:00
|
|
|
|
self._iproxy_fail_count[udid] = 0
|
2025-11-19 20:47:28 +08:00
|
|
|
|
continue
|
2025-10-31 13:19:55 +08:00
|
|
|
|
|
2025-11-20 16:49:37 +08:00
|
|
|
|
# ------ 以下为失败处理 ------
|
|
|
|
|
|
fail += 1
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
self._iproxy_fail_count[udid] = fail
|
|
|
|
|
|
|
|
|
|
|
|
print(f"[Guard] 第 {fail} 次失败 udid={udid}, port={port}")
|
|
|
|
|
|
|
|
|
|
|
|
# ==== 第 1~2 次失败 → 优先重启 iproxy ====
|
|
|
|
|
|
if fail in (1, 2):
|
|
|
|
|
|
print(f"[Guard] 尝试重启 iproxy(第 {fail} 次){udid}")
|
|
|
|
|
|
if self._restart_iproxy(udid, port):
|
|
|
|
|
|
time.sleep(1.5)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# ==== 第 3~4 次失败 → 检查 WDA 状态 ====
|
|
|
|
|
|
if fail in (3, 4):
|
|
|
|
|
|
print(f"[Guard] 检查 WDA 状态 {udid}")
|
|
|
|
|
|
wda_ok = self._wait_wda_ready_on_port(udid, wdaFunctionPort)
|
|
|
|
|
|
|
|
|
|
|
|
if not wda_ok:
|
|
|
|
|
|
print(f"[Guard] WDA 异常 → 尝试重启 WDA(第 {fail} 次){udid}")
|
|
|
|
|
|
if self._restart_wda(udid):
|
|
|
|
|
|
time.sleep(1.2)
|
|
|
|
|
|
continue
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"[Guard] WDA 正常,但视频流挂了 → 再重启 iproxy")
|
|
|
|
|
|
if self._restart_iproxy(udid, port):
|
|
|
|
|
|
time.sleep(1.5)
|
|
|
|
|
|
continue
|
2025-11-19 20:47:28 +08:00
|
|
|
|
|
2025-11-20 16:49:37 +08:00
|
|
|
|
# ==== 第 5 次失败 → 移除设备 ====
|
|
|
|
|
|
if fail >= 5:
|
|
|
|
|
|
print(f"[Guard] 连续 5 次失败,移除设备 {udid}")
|
2025-11-19 20:47:28 +08:00
|
|
|
|
try:
|
2025-11-20 16:49:37 +08:00
|
|
|
|
self._remove_device(udid)
|
2025-11-19 20:47:28 +08:00
|
|
|
|
except Exception as e:
|
2025-11-20 16:49:37 +08:00
|
|
|
|
print(f"[Guard] 移除异常: {e}")
|
2025-11-19 20:47:28 +08:00
|
|
|
|
continue
|
|
|
|
|
|
|
2025-11-20 16:49:37 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[Guard] 守护线程异常: {e}")
|
2025-11-19 20:47:28 +08:00
|
|
|
|
|
2025-11-20 16:49:37 +08:00
|
|
|
|
time.sleep(2.0)
|
2025-11-19 20:47:28 +08:00
|
|
|
|
|
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:
|
|
|
|
|
|
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-11-20 16:49:37 +08:00
|
|
|
|
# 检测设备wda状态
|
|
|
|
|
|
def _wait_wda_ready_on_port(self, udid: str, local_port: int) -> bool:
|
|
|
|
|
|
try:
|
|
|
|
|
|
dev = wda.USBClient(udid, local_port)
|
|
|
|
|
|
info = dev.status() # 调用成功即可说明 WDA 正常
|
|
|
|
|
|
return info["ready"]
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[WDA] status 异常({udid}): {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
2025-10-27 21:44:16 +08:00
|
|
|
|
|
2025-11-14 16:58:55 +08:00
|
|
|
|
def _send_snapshot_to_flask(self):
|
|
|
|
|
|
"""把当前 _models 的全量快照发送给 Flask 进程"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 1. 把 _models 里的设备转成可 JSON 的 dict 列表
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
devices = [m.toDict() for m in self._models.values()]
|
|
|
|
|
|
|
|
|
|
|
|
payload = json.dumps({"devices": devices}, ensure_ascii=False)
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 建立到 Flask 的本地 socket 连接并发送
|
|
|
|
|
|
port = int(os.getenv("FLASK_COMM_PORT", "34566"))
|
|
|
|
|
|
if port <= 0:
|
|
|
|
|
|
LogManager.warning("[SNAPSHOT] 无有效端口,跳过发送")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
with socket.create_connection(("127.0.0.1", port), timeout=1.5) as s:
|
|
|
|
|
|
s.sendall(payload.encode("utf-8") + b"\n")
|
|
|
|
|
|
print(f"[SNAPSHOT] 已发送 {len(devices)} 台设备快照到 Flask")
|
|
|
|
|
|
LogManager.info(f"[SNAPSHOT] 已发送 {len(devices)} 台设备快照到 Flask")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
# 不要让异常影响主循环,只打个日志
|
|
|
|
|
|
LogManager.warning(f"[SNAPSHOT] 发送快照失败: {e}")
|
|
|
|
|
|
|
2025-11-13 19:28:57 +08:00
|
|
|
|
def _device_online_via_tidevice(self, udid: str) -> bool:
|
|
|
|
|
|
try:
|
|
|
|
|
|
from tidevice import Usbmux, ConnectionType
|
|
|
|
|
|
devs = Usbmux().device_list()
|
|
|
|
|
|
return any(d.udid == udid and d.conn_type == ConnectionType.USB for d in devs)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
# 容错:tidevice 异常时,假定在线,避免误判;后续命令会再校验
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def _pair_if_needed_for_ios17(self, udid: str, timeout_sec: float | None = None) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
iOS 17+ 配对:已信任直接 True;否则触发配对并无限等待(设备离线则 False)
|
|
|
|
|
|
使用 “python -m pymobiledevice3 lockdown pair -u <udid>” 做配对,规避 API 版本差异。
|
|
|
|
|
|
timeout_sec=None 表示无限等待;若传入数字则为最多等待秒数。
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 已信任直接过
|
|
|
|
|
|
if self._trusted(udid):
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
print(f"[Pair][CLI] iOS17+ 需要配对,等待手机上点击“信任”… {udid}")
|
|
|
|
|
|
last_log = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
# 轮询直到配对成功/超时/设备离线
|
|
|
|
|
|
while True:
|
|
|
|
|
|
# 1) 设备在线性检查(防止卡等已拔掉的设备)
|
|
|
|
|
|
if not self._device_online_via_tidevice(udid):
|
|
|
|
|
|
print(f"[Pair][CLI] 设备已离线,停止等待 {udid}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
# 2) 触发一次配对尝试
|
|
|
|
|
|
cmd = [sys.executable, "-m", "pymobiledevice3", "lockdown", "pair", "-u", udid]
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 不打印子进程输出,保持你现有日志风格;需要可改为 PIPE 查看
|
|
|
|
|
|
res = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
|
|
if res.returncode == 0:
|
|
|
|
|
|
print(f"[Pair][CLI] 配对成功 {udid}")
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[Pair][CLI] 调用失败:{e}")
|
|
|
|
|
|
|
|
|
|
|
|
# 3) 日志节流 + 可选超时
|
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
|
if now - last_log >= 10.0:
|
|
|
|
|
|
print(f"[Pair][CLI] 仍在等待用户在手机上点“信任”… {udid}")
|
|
|
|
|
|
last_log = now
|
|
|
|
|
|
|
|
|
|
|
|
if timeout_sec is not None:
|
|
|
|
|
|
timeout_sec -= 2.0
|
|
|
|
|
|
if timeout_sec <= 0:
|
|
|
|
|
|
print(f"[Pair][CLI] 等待配对超时(达到设定时长){udid}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
time.sleep(2.0)
|
|
|
|
|
|
|
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
|
2025-11-20 16:49:37 +08:00
|
|
|
|
if not self._wait_wda_ready_on_port(udid, local_port=wdaFunctionPort):
|
2025-11-05 17:07:51 +08:00
|
|
|
|
# 如果还没起来,按你原逻辑拉起 WDA 再等
|
2025-11-13 19:28:57 +08:00
|
|
|
|
if major >= 17:
|
|
|
|
|
|
print("进入 iOS17+ 设备的分支")
|
|
|
|
|
|
if not self._pair_if_needed_for_ios17(udid, timeout_sec=None): # 无限等;传秒数则有限时
|
|
|
|
|
|
print(f"[WDA] iOS17+ 配对失败或设备离线,放弃新增 {udid}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
self._kill(proc)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
self._release_port(port)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
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}")
|
|
|
|
|
|
|
2025-11-20 16:49:37 +08:00
|
|
|
|
if not self._wait_wda_ready_on_port(udid, local_port=wdaFunctionPort):
|
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-11-14 16:58:55 +08:00
|
|
|
|
self._manager_send()
|
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:
|
2025-11-14 16:58:55 +08:00
|
|
|
|
self._manager_send()
|
2025-10-31 13:19:55 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[Remove] 通知上层异常 {udid}: {e}")
|
|
|
|
|
|
|
2025-10-25 00:22:16 +08:00
|
|
|
|
print(f"[Remove] 设备移除完成 {udid}")
|
2025-09-19 14:30:39 +08:00
|
|
|
|
|
2025-09-22 14:36:05 +08:00
|
|
|
|
def _trusted(self, udid: str) -> bool:
|
2025-08-07 20:55:30 +08:00
|
|
|
|
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-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-11-14 16:58:55 +08:00
|
|
|
|
def _manager_send(self):
|
|
|
|
|
|
"""对外统一的“通知 Flask 有设备变动”的入口(无参数)。
|
|
|
|
|
|
作用:把当前所有设备的全量快照发给 Flask。
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 第 1 次:直接发快照
|
2025-09-28 14:35:09 +08:00
|
|
|
|
try:
|
2025-11-14 16:58:55 +08:00
|
|
|
|
self._send_snapshot_to_flask()
|
|
|
|
|
|
return
|
2025-11-05 17:07:51 +08:00
|
|
|
|
except Exception as e:
|
2025-11-14 16:58:55 +08:00
|
|
|
|
print(f"[Manager] 首次发送快照异常: {e}")
|
2025-11-05 21:04:04 +08:00
|
|
|
|
|
2025-11-14 16:58:55 +08:00
|
|
|
|
# 自愈:尝试拉起 Flask 子进程
|
2025-11-05 17:07:51 +08:00
|
|
|
|
try:
|
2025-11-14 16:58:55 +08:00
|
|
|
|
self._manager.start()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[Manager] 拉起 Flask 子进程异常: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
# 第 2 次:再发快照
|
|
|
|
|
|
try:
|
|
|
|
|
|
self._send_snapshot_to_flask()
|
|
|
|
|
|
print(f"[Manager] 重试发送快照成功")
|
2025-10-24 14:36:00 +08:00
|
|
|
|
except Exception as e:
|
2025-11-14 16:58:55 +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}")
|