修复bug
This commit is contained in:
7
.idea/workspace.xml
generated
7
.idea/workspace.xml
generated
@@ -5,15 +5,10 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成">
|
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成">
|
||||||
<change afterPath="$PROJECT_DIR$/Utils/OCRUtils.py" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Module/FlaskService.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/FlaskService.py" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Module/DeviceInfo.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/DeviceInfo.py" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Module/IOSActivator.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/IOSActivator.py" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Module/IOSActivator.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/IOSActivator.py" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Module/Main.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/Main.py" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/Utils/LogManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/LogManager.py" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Utils/LogManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/LogManager.py" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/script/ScriptManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/script/ScriptManager.py" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/script/mac_wda_agent.py" beforeDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/script/windows_run.py" beforeDir="false" />
|
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
|||||||
@@ -20,8 +20,15 @@ import socket
|
|||||||
import http.client
|
import http.client
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import psutil
|
import psutil
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
|
||||||
class DeviceInfo:
|
class DeviceInfo:
|
||||||
|
|
||||||
|
REMOVE_GRACE_SEC = 5.0 # 设备离线宽限期(秒)
|
||||||
|
ADD_STABLE_SEC = 1.5 # 设备上线稳定期(秒)
|
||||||
|
ORPHAN_COOLDOWN = 3.0 # 拓扑变更后暂停孤儿清理(秒)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._port = 9110
|
self._port = 9110
|
||||||
self._models: Dict[str, DeviceModel] = {}
|
self._models: Dict[str, DeviceModel] = {}
|
||||||
@@ -33,38 +40,70 @@ class DeviceInfo:
|
|||||||
self._last_heal_check_ts = 0.0
|
self._last_heal_check_ts = 0.0
|
||||||
self._heal_backoff: Dict[str, float] = defaultdict(float) # udid -> next_allowed_ts
|
self._heal_backoff: Dict[str, float] = defaultdict(float) # udid -> next_allowed_ts
|
||||||
|
|
||||||
|
# 并发保护 & 状态表
|
||||||
|
self._lock = threading.RLock()
|
||||||
|
self._port_by_udid: Dict[str, int] = {} # UDID -> local_port
|
||||||
|
self._pid_by_udid: Dict[str, int] = {} # UDID -> iproxy PID
|
||||||
|
|
||||||
|
# 抗抖:最近一次看到在线的时间 / 首次看到在线的时间
|
||||||
|
self._last_seen: Dict[str, float] = {} # udid -> ts
|
||||||
|
self._first_seen: Dict[str, float] = {} # udid -> ts(首次在线)
|
||||||
|
self._last_topology_change_ts = 0.0 # 最近一次“新增或真正移除”的时间
|
||||||
|
|
||||||
# ---------------- 主循环 ----------------
|
# ---------------- 主循环 ----------------
|
||||||
def listen(self):
|
def listen(self):
|
||||||
orphan_gc_tick = 0
|
orphan_gc_tick = 0
|
||||||
while True:
|
while True:
|
||||||
online = {d.udid for d in Usbmux().device_list() if d.conn_type == ConnectionType.USB}
|
now = time.time()
|
||||||
|
try:
|
||||||
|
usb = Usbmux().device_list()
|
||||||
|
online_now = {d.udid for d in usb if d.conn_type == ConnectionType.USB}
|
||||||
|
except Exception as e:
|
||||||
|
# 如果拉设备列表失败,本轮不做增删(避免误杀)
|
||||||
|
LogManager.warning(f"device_list() 异常:{e}")
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
# 拔掉——同步
|
# 记录“看到”的时间戳
|
||||||
for udid in list(self._models):
|
for u in online_now:
|
||||||
if udid not in online:
|
if u not in self._first_seen:
|
||||||
self._remove_device(udid)
|
self._first_seen[u] = now
|
||||||
|
self._last_seen[u] = now
|
||||||
|
|
||||||
# 插上——异步
|
# 处理真正移除(连续缺席超过宽限期)
|
||||||
new = [u for u in online if u not in self._models]
|
with self._lock:
|
||||||
if new:
|
known = set(self._models.keys())
|
||||||
futures = {self._pool.submit(self._add_device, u): u for u in new}
|
for udid in list(known):
|
||||||
|
last = self._last_seen.get(udid, 0.0)
|
||||||
|
if udid not in online_now and (now - last) >= self.REMOVE_GRACE_SEC:
|
||||||
|
self._remove_device(udid) # 真正下线
|
||||||
|
self._last_topology_change_ts = now
|
||||||
|
|
||||||
|
# 处理真正新增(连续在线超过稳定期)
|
||||||
|
new_candidates = [u for u in online_now if u not in known]
|
||||||
|
to_add = [u for u in new_candidates if (now - self._first_seen.get(u, now)) >= self.ADD_STABLE_SEC]
|
||||||
|
if to_add:
|
||||||
|
futures = {self._pool.submit(self._add_device, u): u for u in to_add}
|
||||||
for f in as_completed(futures, timeout=30):
|
for f in as_completed(futures, timeout=30):
|
||||||
try:
|
try:
|
||||||
f.result()
|
f.result()
|
||||||
|
self._last_topology_change_ts = time.time()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LogManager.error(f"异步连接失败:{e}")
|
LogManager.error(f"异步连接失败:{e}")
|
||||||
|
|
||||||
# 定期健康检查 + 自愈
|
# 定期健康检查 + 自愈
|
||||||
self._check_and_heal_tunnels(interval=2.0)
|
self._check_and_heal_tunnels(interval=2.0)
|
||||||
|
|
||||||
# 每 10 次(约10秒)清理一次孤儿 iproxy
|
# 每 10 次清理一次孤儿 iproxy(但在拓扑变更后 N 秒暂停执行,避免插拔风暴期误杀)
|
||||||
orphan_gc_tick += 1
|
orphan_gc_tick += 1
|
||||||
if orphan_gc_tick >= 10:
|
if orphan_gc_tick >= 10:
|
||||||
orphan_gc_tick = 0
|
orphan_gc_tick = 0
|
||||||
self._cleanup_orphan_iproxy()
|
if (time.time() - self._last_topology_change_ts) >= self.ORPHAN_COOLDOWN:
|
||||||
|
self._cleanup_orphan_iproxy()
|
||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
# ---------------- 新增设备 ----------------
|
# ---------------- 新增设备 ----------------
|
||||||
def _add_device(self, udid: str):
|
def _add_device(self, udid: str):
|
||||||
if not self._trusted(udid):
|
if not self._trusted(udid):
|
||||||
@@ -78,27 +117,38 @@ class DeviceInfo:
|
|||||||
print("未获取到设备屏幕信息")
|
print("未获取到设备屏幕信息")
|
||||||
return
|
return
|
||||||
print("获取设备信息成功")
|
print("获取设备信息成功")
|
||||||
port = self._alloc_port()
|
|
||||||
|
# 固定端口分配(加锁,避免竞态)
|
||||||
|
with self._lock:
|
||||||
|
port = self._alloc_port(udid)
|
||||||
|
|
||||||
proc = self._start_iproxy(udid, port)
|
proc = self._start_iproxy(udid, port)
|
||||||
if not proc:
|
if not proc:
|
||||||
print("启动iproxy失败")
|
print("启动iproxy失败")
|
||||||
return
|
return
|
||||||
model = DeviceModel(deviceId=udid, screenPort=port,
|
|
||||||
width=w, height=h, scale=s, type=1)
|
with self._lock:
|
||||||
model.ready = True
|
model = DeviceModel(deviceId=udid, screenPort=port,
|
||||||
self._models[udid] = model
|
width=w, height=h, scale=s, type=1)
|
||||||
self._procs[udid] = proc
|
model.ready = True
|
||||||
|
self._models[udid] = model
|
||||||
|
self._procs[udid] = proc
|
||||||
|
self._pid_by_udid[udid] = proc.pid
|
||||||
|
|
||||||
print("准备添加设备")
|
print("准备添加设备")
|
||||||
self._manager_send(model)
|
self._manager_send(model)
|
||||||
|
|
||||||
# ---------------- 移除设备 ----------------
|
# ---------------- 移除设备(仅在宽限期后调用) ----------------
|
||||||
def _remove_device(self, udid: str):
|
def _remove_device(self, udid: str):
|
||||||
model = self._models.pop(udid, None)
|
with self._lock:
|
||||||
|
model = self._models.pop(udid, None)
|
||||||
|
proc = self._procs.pop(udid, None)
|
||||||
|
self._pid_by_udid.pop(udid, None)
|
||||||
|
# 不清 _port_by_udid,端口下次仍复用,前端更稳定
|
||||||
if not model:
|
if not model:
|
||||||
return
|
return
|
||||||
model.type = 2
|
model.type = 2
|
||||||
self._kill(self._procs.pop(udid, None))
|
self._kill(proc)
|
||||||
self._manager_send(model)
|
self._manager_send(model)
|
||||||
|
|
||||||
# ---------------- 工具函数 ----------------
|
# ---------------- 工具函数 ----------------
|
||||||
@@ -141,23 +191,42 @@ class DeviceInfo:
|
|||||||
print("获取设备信息遇到错误:", e)
|
print("获取设备信息遇到错误:", e)
|
||||||
return 0, 0, 0
|
return 0, 0, 0
|
||||||
|
|
||||||
...
|
# ---------------- 端口映射(保留你之前的“先杀后启”“隐藏黑窗”修复) ----------------
|
||||||
# ---------------- 原来代码不变,只替换下面一个函数 ----------------
|
|
||||||
def _start_iproxy(self, udid: str, port: int) -> Optional[subprocess.Popen]:
|
def _start_iproxy(self, udid: str, port: int) -> Optional[subprocess.Popen]:
|
||||||
try:
|
try:
|
||||||
# 确保端口空闲;不空闲则尝试换一个
|
with self._lock:
|
||||||
if not self._is_port_free(port):
|
old_pid = self._pid_by_udid.get(udid)
|
||||||
port = self._pick_free_port(max(self._port, port))
|
if old_pid:
|
||||||
|
self._kill_pid_gracefully(old_pid)
|
||||||
|
self._pid_by_udid.pop(udid, None)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
# 隐藏窗口 & 独立进程组(更好地终止)
|
if not self._is_port_free(port):
|
||||||
flags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
|
port = self._pick_free_port(start=max(self._port, port))
|
||||||
|
|
||||||
return subprocess.Popen(
|
creationflags = 0
|
||||||
[self._iproxy_path, "-u", udid, str(port), str(wdaScreenPort)],
|
startupinfo = None
|
||||||
stdout=subprocess.DEVNULL,
|
if os.name == "nt":
|
||||||
stderr=subprocess.DEVNULL,
|
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000) | getattr(subprocess,
|
||||||
creationflags=flags
|
"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(port), str(wdaScreenPort)]
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
creationflags=creationflags,
|
||||||
|
startupinfo=startupinfo
|
||||||
|
)
|
||||||
|
self._procs[udid] = proc
|
||||||
|
self._pid_by_udid[udid] = proc.pid
|
||||||
|
self._port_by_udid[udid] = port
|
||||||
|
return proc
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
return None
|
return None
|
||||||
@@ -174,8 +243,26 @@ class DeviceInfo:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _alloc_port(self) -> int:
|
# ---------------- 端口分配(加锁 + 稳定端口) ----------------
|
||||||
return self._pick_free_port(max(self._port, self._port))
|
def _alloc_port(self, udid: str) -> int:
|
||||||
|
"""
|
||||||
|
为 UDID 分配一个**稳定**的本地端口:
|
||||||
|
- 同一 UDID 优先复用上次端口(减少前端切换)
|
||||||
|
- 初次分配使用 “20000-45000” 的稳定哈希起点向上探测空闲
|
||||||
|
"""
|
||||||
|
# 已有则直接复用
|
||||||
|
if udid in self._port_by_udid:
|
||||||
|
p = self._port_by_udid[udid]
|
||||||
|
if self._is_port_free(p):
|
||||||
|
return p
|
||||||
|
|
||||||
|
# 基于 UDID 计算稳定起点
|
||||||
|
h = int(hashlib.sha1(udid.encode("utf-8")).hexdigest(), 16)
|
||||||
|
start = 20000 + (h % 25000) # 20000~44999
|
||||||
|
# 避免和你类里默认的 9110 等端口冲突,向上找空闲
|
||||||
|
p = self._pick_free_port(start=start, limit=4000)
|
||||||
|
self._port_by_udid[udid] = p
|
||||||
|
return p
|
||||||
|
|
||||||
def _manager_send(self, model: DeviceModel):
|
def _manager_send(self, model: DeviceModel):
|
||||||
try:
|
try:
|
||||||
@@ -192,22 +279,23 @@ class DeviceInfo:
|
|||||||
return str(path)
|
return str(path)
|
||||||
raise FileNotFoundError(f"iproxy 不存在: {path}")
|
raise FileNotFoundError(f"iproxy 不存在: {path}")
|
||||||
|
|
||||||
# ------------ Windows 专用:列出所有 iproxy 命令行 ------------
|
# ------------ Windows 专用:列出所有 iproxy 命令行(更安全) ------------
|
||||||
def _get_all_iproxy_cmdlines(self) -> List[str]:
|
def _get_all_iproxy_cmdlines(self) -> List[str]:
|
||||||
"""
|
|
||||||
使用 psutil 枚举 iproxy 进程,避免调用 wmic 造成的黑框闪烁。
|
|
||||||
返回形如:"<完整命令行> <pid>" 的列表(兼容你后续的解析逻辑)。
|
|
||||||
"""
|
|
||||||
lines: List[str] = []
|
lines: List[str] = []
|
||||||
|
live_pids = set()
|
||||||
|
with self._lock:
|
||||||
|
live_pids = set(self._pid_by_udid.values())
|
||||||
for p in psutil.process_iter(attrs=["name", "cmdline", "pid"]):
|
for p in psutil.process_iter(attrs=["name", "cmdline", "pid"]):
|
||||||
try:
|
try:
|
||||||
name = (p.info.get("name") or "").lower()
|
name = (p.info.get("name") or "").lower()
|
||||||
if name != "iproxy.exe":
|
if name != "iproxy.exe":
|
||||||
continue
|
continue
|
||||||
|
# 跳过我们自己登记在册的 iproxy,避免误杀
|
||||||
|
if p.info["pid"] in live_pids:
|
||||||
|
continue
|
||||||
cmdline = p.info.get("cmdline") or []
|
cmdline = p.info.get("cmdline") or []
|
||||||
if not cmdline:
|
if not cmdline:
|
||||||
continue
|
continue
|
||||||
# 与原逻辑保持一致:仅收集包含 -u 的 iproxy(我们需要解析 udid)
|
|
||||||
if "-u" in cmdline:
|
if "-u" in cmdline:
|
||||||
cmd = " ".join(cmdline)
|
cmd = " ".join(cmdline)
|
||||||
lines.append(f"{cmd} {p.info['pid']}")
|
lines.append(f"{cmd} {p.info['pid']}")
|
||||||
@@ -217,13 +305,19 @@ class DeviceInfo:
|
|||||||
|
|
||||||
# ------------ 杀孤儿 ------------
|
# ------------ 杀孤儿 ------------
|
||||||
def _cleanup_orphan_iproxy(self):
|
def _cleanup_orphan_iproxy(self):
|
||||||
live_udids = set(self._models.keys())
|
live_udids = set()
|
||||||
|
live_pids = set()
|
||||||
|
with self._lock:
|
||||||
|
live_udids = set(self._models.keys())
|
||||||
|
live_pids = set(self._pid_by_udid.values())
|
||||||
|
|
||||||
for ln in self._get_all_iproxy_cmdlines():
|
for ln in self._get_all_iproxy_cmdlines():
|
||||||
parts = ln.split()
|
parts = ln.split()
|
||||||
try:
|
try:
|
||||||
udid = parts[parts.index('-u') + 1]
|
udid = parts[parts.index('-u') + 1]
|
||||||
pid = int(parts[-1])
|
pid = int(parts[-1])
|
||||||
if udid not in live_udids:
|
# 既不在我们的 PID 表里,且 UDID 不在线,才算孤儿
|
||||||
|
if pid not in live_pids and udid not in live_udids:
|
||||||
self._kill_pid_gracefully(pid)
|
self._kill_pid_gracefully(pid)
|
||||||
LogManager.warning(f'扫到孤儿 iproxy,已清理 {udid} PID={pid}')
|
LogManager.warning(f'扫到孤儿 iproxy,已清理 {udid} PID={pid}')
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
@@ -241,8 +335,7 @@ class DeviceInfo:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# ------------ 端口工具 ------------
|
||||||
|
|
||||||
def _is_port_free(self, port: int) -> bool:
|
def _is_port_free(self, port: int) -> bool:
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
@@ -254,7 +347,7 @@ class DeviceInfo:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def _pick_free_port(self, start: int = None, limit: int = 2000) -> int:
|
def _pick_free_port(self, start: int = None, limit: int = 2000) -> int:
|
||||||
"""从 start 起向上找一个空闲端口。"""
|
"""从 start 起向上找一个空闲端口。(注意:调用方务必在 self._lock 下)"""
|
||||||
p = self._port if start is None else start
|
p = self._port if start is None else start
|
||||||
tried = 0
|
tried = 0
|
||||||
while tried < limit:
|
while tried < limit:
|
||||||
@@ -274,9 +367,7 @@ class DeviceInfo:
|
|||||||
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=timeout)
|
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=timeout)
|
||||||
conn.request("GET", "/")
|
conn.request("GET", "/")
|
||||||
resp = conn.getresponse()
|
resp = conn.getresponse()
|
||||||
# 2xx/3xx 都算活;某些构建下会是 200 带 multipart,也可能 302
|
|
||||||
alive = 200 <= resp.status < 400
|
alive = 200 <= resp.status < 400
|
||||||
# 尽量少读:只读很少字节避免成本
|
|
||||||
try:
|
try:
|
||||||
resp.read(256)
|
resp.read(256)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -287,36 +378,39 @@ class DeviceInfo:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def _restart_iproxy(self, udid: str):
|
def _restart_iproxy(self, udid: str):
|
||||||
"""重启某个 udid 的 iproxy(带退避)"""
|
"""重启某个 udid 的 iproxy(带退避)。"""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
next_allowed = self._heal_backoff[udid]
|
next_allowed = self._heal_backoff[udid]
|
||||||
if now < next_allowed:
|
if now < next_allowed:
|
||||||
return # 处于退避窗口内,先不重启
|
return # 处于退避窗口内,先不重启
|
||||||
|
|
||||||
proc = self._procs.get(udid)
|
with self._lock:
|
||||||
if proc:
|
proc = self._procs.get(udid)
|
||||||
self._kill(proc)
|
if proc:
|
||||||
# 让端口真正释放
|
self._kill(proc)
|
||||||
time.sleep(0.3)
|
time.sleep(0.3)
|
||||||
|
|
||||||
model = self._models.get(udid)
|
model = self._models.get(udid)
|
||||||
if not model:
|
if not model:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 如果端口被别的进程占用了,换一个新端口并通知管理器
|
# 如果端口被别的进程占用了,换一个新端口并通知管理器
|
||||||
if not self._is_port_free(model.screenPort):
|
if not self._is_port_free(model.screenPort):
|
||||||
new_port = self._pick_free_port(max(self._port, model.screenPort))
|
new_port = self._pick_free_port(start=max(self._port, model.screenPort))
|
||||||
model.screenPort = new_port
|
model.screenPort = new_port
|
||||||
self._models[udid] = model
|
self._models[udid] = model
|
||||||
self._manager_send(model) # 通知前端/上位机端口变化
|
self._port_by_udid[udid] = new_port
|
||||||
|
self._manager_send(model) # 通知前端/上位机端口变化
|
||||||
|
|
||||||
proc2 = self._start_iproxy(udid, model.screenPort)
|
proc2 = self._start_iproxy(udid, model.screenPort)
|
||||||
if not proc2:
|
if not proc2:
|
||||||
# 启动失败,设置退避(逐步增加上限)
|
# 启动失败,设置退避(逐步增加上限)
|
||||||
self._heal_backoff[udid] = now + 2.0
|
self._heal_backoff[udid] = now + 2.0
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._procs[udid] = proc2
|
||||||
|
self._pid_by_udid[udid] = proc2.pid
|
||||||
|
|
||||||
self._procs[udid] = proc2
|
|
||||||
# 成功后缩短退避
|
# 成功后缩短退避
|
||||||
self._heal_backoff[udid] = now + 0.5
|
self._heal_backoff[udid] = now + 0.5
|
||||||
|
|
||||||
@@ -329,7 +423,11 @@ class DeviceInfo:
|
|||||||
return
|
return
|
||||||
self._last_heal_check_ts = now
|
self._last_heal_check_ts = now
|
||||||
|
|
||||||
for udid, model in list(self._models.items()):
|
# 读取时也加锁,避免与增删设备并发冲突
|
||||||
|
with self._lock:
|
||||||
|
items = list(self._models.items())
|
||||||
|
|
||||||
|
for udid, model in items:
|
||||||
port = model.screenPort
|
port = model.screenPort
|
||||||
if port <= 0:
|
if port <= 0:
|
||||||
continue
|
continue
|
||||||
@@ -337,4 +435,3 @@ class DeviceInfo:
|
|||||||
if not ok:
|
if not ok:
|
||||||
LogManager.warning(f"端口失活,准备自愈:udid={udid} port={port}")
|
LogManager.warning(f"端口失活,准备自愈:udid={udid} port={port}")
|
||||||
self._restart_iproxy(udid)
|
self._restart_iproxy(udid)
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,13 @@ class IOSActivator:
|
|||||||
cmd = [*launcher, *args]
|
cmd = [*launcher, *args]
|
||||||
print("[pmd3]", " ".join(map(str, cmd)))
|
print("[pmd3]", " ".join(map(str, cmd)))
|
||||||
try:
|
try:
|
||||||
return subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT, env=env) or ""
|
return subprocess.check_output(
|
||||||
|
cmd,
|
||||||
|
text=True,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
env=env,
|
||||||
|
**self._win_hidden_popen_kwargs()
|
||||||
|
) or ""
|
||||||
except subprocess.CalledProcessError as exc:
|
except subprocess.CalledProcessError as exc:
|
||||||
raise RuntimeError(exc.output or f"pymobiledevice3 执行失败,退出码 {exc.returncode}")
|
raise RuntimeError(exc.output or f"pymobiledevice3 执行失败,退出码 {exc.returncode}")
|
||||||
|
|
||||||
@@ -62,12 +68,17 @@ class IOSActivator:
|
|||||||
cmd = self._ensure_str_list(cmd)
|
cmd = self._ensure_str_list(cmd)
|
||||||
print("[pmd3-subproc]", " ".join(cmd))
|
print("[pmd3-subproc]", " ".join(cmd))
|
||||||
try:
|
try:
|
||||||
out = subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT, env=env)
|
out = subprocess.check_output(
|
||||||
|
cmd,
|
||||||
|
text=True,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
env=env,
|
||||||
|
**self._win_hidden_popen_kwargs()
|
||||||
|
)
|
||||||
return out or ""
|
return out or ""
|
||||||
except subprocess.CalledProcessError as exc:
|
except subprocess.CalledProcessError as exc:
|
||||||
raise RuntimeError(exc.output or f"pymobiledevice3 子进程执行失败,代码 {exc.returncode}")
|
raise RuntimeError(exc.output or f"pymobiledevice3 子进程执行失败,代码 {exc.returncode}")
|
||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# 旧版环境依赖的替代方案:仅用于启动 tunneld 的子进程(需要常驻)
|
# 旧版环境依赖的替代方案:仅用于启动 tunneld 的子进程(需要常驻)
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -105,7 +116,11 @@ class IOSActivator:
|
|||||||
try:
|
try:
|
||||||
out = subprocess.check_output(
|
out = subprocess.check_output(
|
||||||
[str(cand), "-c", "import pymobiledevice3;print('ok')"],
|
[str(cand), "-c", "import pymobiledevice3;print('ok')"],
|
||||||
text=True, stderr=subprocess.STDOUT, env=env, timeout=6
|
text=True,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
env=env,
|
||||||
|
timeout=6,
|
||||||
|
**self._win_hidden_popen_kwargs()
|
||||||
)
|
)
|
||||||
if "ok" in out:
|
if "ok" in out:
|
||||||
print(f"[IOSAI] ✅ sidecar selected: {cand}")
|
print(f"[IOSAI] ✅ sidecar selected: {cand}")
|
||||||
@@ -135,7 +150,11 @@ class IOSActivator:
|
|||||||
try:
|
try:
|
||||||
out = subprocess.check_output(
|
out = subprocess.check_output(
|
||||||
[py, "-c", "import pymobiledevice3;print('ok')"],
|
[py, "-c", "import pymobiledevice3;print('ok')"],
|
||||||
text=True, stderr=subprocess.STDOUT, env=env, timeout=6
|
text=True,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
env=env,
|
||||||
|
timeout=6,
|
||||||
|
**self._win_hidden_popen_kwargs()
|
||||||
)
|
)
|
||||||
if "ok" in out:
|
if "ok" in out:
|
||||||
print(f"[IOSAI] ✅ system python selected: {py}")
|
print(f"[IOSAI] ✅ system python selected: {py}")
|
||||||
@@ -145,15 +164,26 @@ class IOSActivator:
|
|||||||
|
|
||||||
raise RuntimeError("未检测到可用的 pymobiledevice3(建议携带 python-rt 或安装系统 Python+pmd3)。")
|
raise RuntimeError("未检测到可用的 pymobiledevice3(建议携带 python-rt 或安装系统 Python+pmd3)。")
|
||||||
|
|
||||||
|
|
||||||
def _ensure_str_list(self, seq):
|
def _ensure_str_list(self, seq):
|
||||||
return [str(x) for x in seq]
|
return [str(x) for x in seq]
|
||||||
|
|
||||||
|
def _win_hidden_popen_kwargs(self):
|
||||||
|
"""在 Windows 上隐藏子进程窗口;非 Windows 返回空参数。"""
|
||||||
|
if os.name != "nt":
|
||||||
|
return {}
|
||||||
|
import subprocess as _sp
|
||||||
|
si = _sp.STARTUPINFO()
|
||||||
|
si.dwFlags |= _sp.STARTF_USESHOWWINDOW
|
||||||
|
si.wShowWindow = 0 # SW_HIDE
|
||||||
|
return {
|
||||||
|
"startupinfo": si,
|
||||||
|
"creationflags": getattr(_sp, "CREATE_NO_WINDOW", 0x08000000),
|
||||||
|
}
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# 功能函数-
|
# 功能函数-
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
def _auto_mount_developer_disk(self, udid: str, retries: int = 3, backoff_seconds: float = 2.0) -> None:
|
def _auto_mount_developer_disk(self, udid: str, retries: int = 3, backoff_seconds: float = 2.0) -> None:
|
||||||
print("[mounter] Developer disk image mounted.")
|
|
||||||
"""
|
"""
|
||||||
使用进程内 CLI:pymobiledevice3 mounter auto-mount(带重试)
|
使用进程内 CLI:pymobiledevice3 mounter auto-mount(带重试)
|
||||||
"""
|
"""
|
||||||
@@ -310,6 +340,7 @@ class IOSActivator:
|
|||||||
bufsize=1,
|
bufsize=1,
|
||||||
universal_newlines=True,
|
universal_newlines=True,
|
||||||
env=env2,
|
env=env2,
|
||||||
|
**self._win_hidden_popen_kwargs()
|
||||||
)
|
)
|
||||||
|
|
||||||
captured: list[str] = []
|
captured: list[str] = []
|
||||||
@@ -440,5 +471,3 @@ class IOSActivator:
|
|||||||
return udid.lower() in (line or "").lower()
|
return udid.lower() in (line or "").lower()
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -26,7 +26,7 @@ def _force_utf8_everywhere():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
_force_utf8_everywhere()
|
# _force_utf8_everywhere()
|
||||||
|
|
||||||
class LogManager:
|
class LogManager:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user