优化僵尸iproxy进程
This commit is contained in:
3
.idea/workspace.xml
generated
3
.idea/workspace.xml
generated
@@ -6,7 +6,8 @@
|
||||
<component name="ChangeListManager">
|
||||
<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$/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>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
|
||||
@@ -14,6 +14,10 @@ from Entity.Variables import WdaAppBundleId
|
||||
from Module.FlaskSubprocessManager import FlaskSubprocessManager
|
||||
from Utils.LogManager import LogManager
|
||||
|
||||
import socket
|
||||
import http.client
|
||||
from collections import defaultdict
|
||||
import psutil
|
||||
|
||||
class DeviceInfo:
|
||||
def __init__(self):
|
||||
@@ -24,14 +28,20 @@ class DeviceInfo:
|
||||
self._iproxy_path = self._find_iproxy()
|
||||
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):
|
||||
orphan_gc_tick = 0
|
||||
while True:
|
||||
online = {d.udid for d in Usbmux().device_list() if d.conn_type == ConnectionType.USB}
|
||||
|
||||
# 拔掉——同步
|
||||
for udid in list(self._models):
|
||||
if udid not in online:
|
||||
self._remove_device(udid)
|
||||
|
||||
# 插上——异步
|
||||
new = [u for u in online if u not in self._models]
|
||||
if new:
|
||||
@@ -41,6 +51,16 @@ class DeviceInfo:
|
||||
f.result()
|
||||
except Exception as 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)
|
||||
|
||||
# ---------------- 新增设备 ----------------
|
||||
@@ -100,25 +120,29 @@ class DeviceInfo:
|
||||
try:
|
||||
c = wda.USBClient(udid, 8100)
|
||||
c.home()
|
||||
size = c.window_size()
|
||||
size = c.window_size()
|
||||
scale = c.scale
|
||||
return int(size.width), int(size.height), float(scale)
|
||||
return 0, 0, 0
|
||||
except Exception as e:
|
||||
print("获取设备信息遇到错误:",e)
|
||||
print("获取设备信息遇到错误:", e)
|
||||
return 0, 0, 0
|
||||
|
||||
...
|
||||
# ---------------- 原来代码不变,只替换下面一个函数 ----------------
|
||||
def _start_iproxy(self, udid: str, port: int) -> Optional[subprocess.Popen]:
|
||||
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(
|
||||
[self._iproxy_path, "-u", udid, str(port), "9100"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
**kw
|
||||
creationflags=flags
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
@@ -137,8 +161,7 @@ class DeviceInfo:
|
||||
pass
|
||||
|
||||
def _alloc_port(self) -> int:
|
||||
self._port += 1
|
||||
return self._port
|
||||
return self._pick_free_port(max(self._port, self._port))
|
||||
|
||||
def _manager_send(self, model: DeviceModel):
|
||||
try:
|
||||
@@ -157,25 +180,25 @@ class DeviceInfo:
|
||||
|
||||
# ------------ Windows 专用:列出所有 iproxy 命令行 ------------
|
||||
def _get_all_iproxy_cmdlines(self) -> List[str]:
|
||||
try:
|
||||
raw = subprocess.check_output(
|
||||
['wmic', 'process', 'where', "name='iproxy.exe'",
|
||||
'get', 'CommandLine,ProcessId', '/value'],
|
||||
stderr=subprocess.DEVNULL, text=True
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
return []
|
||||
"""
|
||||
使用 psutil 枚举 iproxy 进程,避免调用 wmic 造成的黑框闪烁。
|
||||
返回形如:"<完整命令行> <pid>" 的列表(兼容你后续的解析逻辑)。
|
||||
"""
|
||||
lines: List[str] = []
|
||||
for block in raw.split('\n\n'):
|
||||
cmd = pid = ''
|
||||
for line in block.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith('CommandLine='):
|
||||
cmd = line[len('CommandLine='):].strip()
|
||||
elif line.startswith('ProcessId='):
|
||||
pid = line[len('ProcessId='):].strip()
|
||||
if cmd and pid and '-u' in cmd:
|
||||
lines.append(f'{cmd} {pid}')
|
||||
for p in psutil.process_iter(attrs=["name", "cmdline", "pid"]):
|
||||
try:
|
||||
name = (p.info.get("name") or "").lower()
|
||||
if name != "iproxy.exe":
|
||||
continue
|
||||
cmdline = p.info.get("cmdline") or []
|
||||
if not cmdline:
|
||||
continue
|
||||
# 与原逻辑保持一致:仅收集包含 -u 的 iproxy(我们需要解析 udid)
|
||||
if "-u" in cmdline:
|
||||
cmd = " ".join(cmdline)
|
||||
lines.append(f"{cmd} {p.info['pid']}")
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
return lines
|
||||
|
||||
# ------------ 杀孤儿 ------------
|
||||
@@ -195,9 +218,109 @@ class DeviceInfo:
|
||||
# ------------ 按 PID 强杀 ------------
|
||||
def _kill_pid_gracefully(self, pid: int):
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
time.sleep(1)
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
p = psutil.Process(pid)
|
||||
p.terminate()
|
||||
try:
|
||||
p.wait(timeout=1.0)
|
||||
except psutil.TimeoutExpired:
|
||||
p.kill()
|
||||
except Exception:
|
||||
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)
|
||||
|
||||
|
||||
Binary file not shown.
@@ -10,5 +10,4 @@ python -m nuitka Module\Main.py ^
|
||||
--include-data-files="E:/code/Python/iOSAi/resources/iproxy/*=resources/iproxy/" ^
|
||||
--include-data-files=resources/icon.ico=resources/icon.ico ^
|
||||
--jobs=20 ^
|
||||
--windows-icon-from-ico=resources/icon.ico
|
||||
|
||||
--windows-icon-from-ico=resources/icon.ico
|
||||
Reference in New Issue
Block a user