优化僵尸iproxy进程

This commit is contained in:
2025-09-28 14:35:09 +08:00
parent 67e0df8af9
commit d876743d3e
4 changed files with 155 additions and 32 deletions

3
.idea/workspace.xml generated
View File

@@ -6,7 +6,8 @@
<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 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/Main.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/Main.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Module/DeviceInfo.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/DeviceInfo.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/build.bat" beforeDir="false" afterPath="$PROJECT_DIR$/build.bat" afterDir="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" />

View File

@@ -14,6 +14,10 @@ from Entity.Variables import WdaAppBundleId
from Module.FlaskSubprocessManager import FlaskSubprocessManager from Module.FlaskSubprocessManager import FlaskSubprocessManager
from Utils.LogManager import LogManager from Utils.LogManager import LogManager
import socket
import http.client
from collections import defaultdict
import psutil
class DeviceInfo: class DeviceInfo:
def __init__(self): def __init__(self):
@@ -24,14 +28,20 @@ class DeviceInfo:
self._iproxy_path = self._find_iproxy() self._iproxy_path = self._find_iproxy()
self._pool = ThreadPoolExecutor(max_workers=6) self._pool = ThreadPoolExecutor(max_workers=6)
self._last_heal_check_ts = 0.0
self._heal_backoff: Dict[str, float] = defaultdict(float) # udid -> next_allowed_ts
# ---------------- 主循环 ---------------- # ---------------- 主循环 ----------------
def listen(self): def listen(self):
orphan_gc_tick = 0
while True: while True:
online = {d.udid for d in Usbmux().device_list() if d.conn_type == ConnectionType.USB} online = {d.udid for d in Usbmux().device_list() if d.conn_type == ConnectionType.USB}
# 拔掉——同步 # 拔掉——同步
for udid in list(self._models): for udid in list(self._models):
if udid not in online: if udid not in online:
self._remove_device(udid) self._remove_device(udid)
# 插上——异步 # 插上——异步
new = [u for u in online if u not in self._models] new = [u for u in online if u not in self._models]
if new: if new:
@@ -41,6 +51,16 @@ class DeviceInfo:
f.result() f.result()
except Exception as e: except Exception as e:
LogManager.error(f"异步连接失败:{e}") LogManager.error(f"异步连接失败:{e}")
# 定期健康检查 + 自愈
self._check_and_heal_tunnels(interval=2.0)
# 每 10 次约10秒清理一次孤儿 iproxy
orphan_gc_tick += 1
if orphan_gc_tick >= 10:
orphan_gc_tick = 0
self._cleanup_orphan_iproxy()
time.sleep(1) time.sleep(1)
# ---------------- 新增设备 ---------------- # ---------------- 新增设备 ----------------
@@ -103,7 +123,6 @@ class DeviceInfo:
size = c.window_size() size = c.window_size()
scale = c.scale scale = c.scale
return int(size.width), int(size.height), float(scale) return int(size.width), int(size.height), float(scale)
return 0, 0, 0
except Exception as e: except Exception as e:
print("获取设备信息遇到错误:", e) print("获取设备信息遇到错误:", e)
return 0, 0, 0 return 0, 0, 0
@@ -112,13 +131,18 @@ class DeviceInfo:
# ---------------- 原来代码不变,只替换下面一个函数 ---------------- # ---------------- 原来代码不变,只替换下面一个函数 ----------------
def _start_iproxy(self, udid: str, port: int) -> Optional[subprocess.Popen]: def _start_iproxy(self, udid: str, port: int) -> Optional[subprocess.Popen]:
try: try:
# 隐藏窗口的核心参数 # 确保端口空闲;不空闲则尝试换一个
kw = {"creationflags": subprocess.CREATE_NO_WINDOW} if not self._is_port_free(port):
port = self._pick_free_port(max(self._port, port))
# 隐藏窗口 & 独立进程组(更好地终止)
flags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
return subprocess.Popen( return subprocess.Popen(
[self._iproxy_path, "-u", udid, str(port), "9100"], [self._iproxy_path, "-u", udid, str(port), "9100"],
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
**kw creationflags=flags
) )
except Exception as e: except Exception as e:
print(e) print(e)
@@ -137,8 +161,7 @@ class DeviceInfo:
pass pass
def _alloc_port(self) -> int: def _alloc_port(self) -> int:
self._port += 1 return self._pick_free_port(max(self._port, self._port))
return self._port
def _manager_send(self, model: DeviceModel): def _manager_send(self, model: DeviceModel):
try: try:
@@ -157,25 +180,25 @@ class DeviceInfo:
# ------------ Windows 专用:列出所有 iproxy 命令行 ------------ # ------------ Windows 专用:列出所有 iproxy 命令行 ------------
def _get_all_iproxy_cmdlines(self) -> List[str]: def _get_all_iproxy_cmdlines(self) -> List[str]:
try: """
raw = subprocess.check_output( 使用 psutil 枚举 iproxy 进程,避免调用 wmic 造成的黑框闪烁。
['wmic', 'process', 'where', "name='iproxy.exe'", 返回形如:"<完整命令行> <pid>" 的列表(兼容你后续的解析逻辑)。
'get', 'CommandLine,ProcessId', '/value'], """
stderr=subprocess.DEVNULL, text=True
)
except subprocess.CalledProcessError:
return []
lines: List[str] = [] lines: List[str] = []
for block in raw.split('\n\n'): for p in psutil.process_iter(attrs=["name", "cmdline", "pid"]):
cmd = pid = '' try:
for line in block.splitlines(): name = (p.info.get("name") or "").lower()
line = line.strip() if name != "iproxy.exe":
if line.startswith('CommandLine='): continue
cmd = line[len('CommandLine='):].strip() cmdline = p.info.get("cmdline") or []
elif line.startswith('ProcessId='): if not cmdline:
pid = line[len('ProcessId='):].strip() continue
if cmd and pid and '-u' in cmd: # 与原逻辑保持一致:仅收集包含 -u 的 iproxy我们需要解析 udid
lines.append(f'{cmd} {pid}') if "-u" in cmdline:
cmd = " ".join(cmdline)
lines.append(f"{cmd} {p.info['pid']}")
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
return lines return lines
# ------------ 杀孤儿 ------------ # ------------ 杀孤儿 ------------
@@ -195,9 +218,109 @@ class DeviceInfo:
# ------------ 按 PID 强杀 ------------ # ------------ 按 PID 强杀 ------------
def _kill_pid_gracefully(self, pid: int): def _kill_pid_gracefully(self, pid: int):
try: try:
os.kill(pid, signal.SIGTERM) p = psutil.Process(pid)
time.sleep(1) p.terminate()
os.kill(pid, signal.SIGKILL) try:
p.wait(timeout=1.0)
except psutil.TimeoutExpired:
p.kill()
except Exception: except Exception:
pass pass
def _is_port_free(self, port: int) -> bool:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.settimeout(0.2)
try:
s.bind(("127.0.0.1", port))
return True
except OSError:
return False
def _pick_free_port(self, start: int = None, limit: int = 2000) -> int:
"""从 start 起向上找一个空闲端口。"""
p = self._port if start is None else start
tried = 0
while tried < limit:
p += 1
tried += 1
if self._is_port_free(p):
self._port = p # 更新游标
return p
raise RuntimeError("未找到可用端口(扫描范围内)")
def _health_check_mjpeg(self, port: int, timeout: float = 1.0) -> bool:
"""
对 http://127.0.0.1:<port>/ 做非常轻量的探活。
WDA mjpegServer(默认9100)通常根路径就会有 multipart/x-mixed-replace。
"""
try:
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=timeout)
conn.request("GET", "/")
resp = conn.getresponse()
# 2xx/3xx 都算活;某些构建下会是 200 带 multipart也可能 302
alive = 200 <= resp.status < 400
# 尽量少读:只读很少字节避免成本
try:
resp.read(256)
except Exception:
pass
conn.close()
return alive
except Exception:
return False
def _restart_iproxy(self, udid: str):
"""重启某个 udid 的 iproxy带退避"""
now = time.time()
next_allowed = self._heal_backoff[udid]
if now < next_allowed:
return # 处于退避窗口内,先不重启
proc = self._procs.get(udid)
if proc:
self._kill(proc)
# 让端口真正释放
time.sleep(0.3)
model = self._models.get(udid)
if not model:
return
# 如果端口被别的进程占用了,换一个新端口并通知管理器
if not self._is_port_free(model.screenPort):
new_port = self._pick_free_port(max(self._port, model.screenPort))
model.screenPort = new_port
self._models[udid] = model
self._manager_send(model) # 通知前端/上位机端口变化
proc2 = self._start_iproxy(udid, model.screenPort)
if not proc2:
# 启动失败,设置退避(逐步增加上限)
self._heal_backoff[udid] = now + 2.0
return
self._procs[udid] = proc2
# 成功后缩短退避
self._heal_backoff[udid] = now + 0.5
def _check_and_heal_tunnels(self, interval: float = 2.0):
"""
定期巡检所有在线设备的本地映射端口是否“活着”,不活就重启 iproxy。
"""
now = time.time()
if now - self._last_heal_check_ts < interval:
return
self._last_heal_check_ts = now
for udid, model in list(self._models.items()):
port = model.screenPort
if port <= 0:
continue
ok = self._health_check_mjpeg(port, timeout=0.8)
if not ok:
LogManager.warning(f"端口失活准备自愈udid={udid} port={port}")
self._restart_iproxy(udid)

View File

@@ -11,4 +11,3 @@ python -m nuitka Module\Main.py ^
--include-data-files=resources/icon.ico=resources/icon.ico ^ --include-data-files=resources/icon.ico=resources/icon.ico ^
--jobs=20 ^ --jobs=20 ^
--windows-icon-from-ico=resources/icon.ico --windows-icon-from-ico=resources/icon.ico