diff --git a/Module/DeviceInfo.py b/Module/DeviceInfo.py index a5a38ce..d4f8d74 100644 --- a/Module/DeviceInfo.py +++ b/Module/DeviceInfo.py @@ -174,57 +174,54 @@ class Deviceinfo(object): try: self.manager.send(model.toDict()) except Exception as e: - LogManager.warning(f"发送上线事件失败:{e}", model.deviceId) - LogManager.info(f"设备上线,当前在线数:{len(self.deviceModelList)}", model.deviceId) + LogManager.warning(f"{model.deviceId} 发送上线事件失败:{e}") + LogManager.info(f"{model.deviceId} 设备上线,当前在线数:{len(self.deviceModelList)}") + # 删除设备 def _remove_model(self, udid: str): - print("进入删除方法") + """线程安全:把设备从内存、端口、iproxy 完全抹掉""" model = self._model_index.pop(udid, None) - print(model) if not model: - print("没有找到model") + LogManager.error(f"没有找到需要删除的设备 {udid}") return model.type = 2 - # 内存结构删除 + # 1. 内存列表删除 try: - idx = self.deviceModelList.index(model) - self.deviceModelList.pop(idx) + self.deviceModelList.remove(model) except ValueError: - print("有错误了") pass - # 端口回收(关键) + # 2. 端口回收 self._free_port(model.screenPort) - # 清理 iproxy - survivors = [item for item in self.pidList if item.get("id") != udid] + # 3. 原子化清理 iproxy(确保进程彻底死) + to_kill, survivors = [], [] for item in self.pidList: - if item.get("id") == udid: - self._terminate_proc(item.get("target")) - self.pidList = survivors + (to_kill if item.get("id") == udid else survivors).append(item) - # Socket 发送(无锁) + for item in to_kill: + self._terminate_proc(item.get("target")) # 下面也给了加强版实现 + + self.pidList = survivors + LogManager.info(f"设备下线 {udid},当前在线数:{len(self.deviceModelList)}") + + # 4. 发 Socket retry = 3 while retry: try: self.manager.send(model.toDict()) - print("删除了") break except Exception as e: - print("有问题了", e) retry -= 1 - LogManager.error(f"发送下线事件失败,剩余重试 {retry}:{e}", udid) + LogManager.error(f"发送下线事件失败,{udid} 剩余重试 {retry}:{e}") time.sleep(0.2) else: - LogManager.error("发送下线事件彻底失败,主动崩溃防止状态不一致", udid) + LogManager.error(f"发送下线事件彻底失败,{udid} 主动崩溃防止状态不一致") os._exit(1) - LogManager.info(f"设备下线,当前在线数:{len(self.deviceModelList)}", udid) - # region ===================== 端口分配与回收 ===================== def _alloc_port(self) -> int: - print(self.screenProxy) if self._port_pool: port = self._port_pool.pop() else: @@ -289,17 +286,63 @@ class Deviceinfo(object): return False def relayDeviceScreenPort(self, udid: str, port: int) -> Optional[subprocess.Popen]: + """启动 iproxy 前:端口若仍被占用则先杀掉占用者,再启动""" if not self._spawn_iproxy: - LogManager.error("iproxy 启动器未就绪,无法建立端口映射", udid) + LogManager.error("iproxy 启动器未就绪", udid) return None + + # --- 新增:端口冲突检查 + 强制清理 --- + while self._port_in_use and self._is_port_open(port): + # 先查是哪个进程占用 + pid = self._get_pid_by_port(port) + if pid and pid != os.getpid(): + LogManager.warning(f"端口 {port} 仍被 PID {pid} 占用,尝试释放", udid) + self._kill_pid_gracefully(pid) + else: + break + # ------------------------------------- + try: p = self._spawn_iproxy(udid, port, 9100) + self._port_in_use.add(port) LogManager.info(f"启动 iproxy 成功,本地 {port} -> 设备 9100", udid) return p except Exception as e: LogManager.error(f"启动 iproxy 失败:{e}", udid) return None + # ------------------- 新增三个小工具 ------------------- + def _is_port_open(self, port: int) -> bool: + import socket + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + return s.connect_ex(("127.0.0.1", port)) == 0 + + def _get_pid_by_port(self, port: int) -> Optional[int]: + """跨平台根据端口号查 PID,失败返回 None""" + try: + if os.name == "nt": + cmd = ["netstat", "-ano", "-p", "tcp"] + out = subprocess.check_output(cmd, text=True) + for line in out.splitlines(): + if f"127.0.0.1:{port}" in line and "LISTENING" in line: + return int(line.strip().split()[-1]) + else: + cmd = ["lsof", "-t", f"-iTCP:{port}", "-sTCP:LISTEN"] + out = subprocess.check_output(cmd, text=True) + return int(out.strip().split()[0]) + except Exception: + return None + + def _kill_pid_gracefully(self, pid: int): + """先 terminate 再 kill -9""" + try: + os.kill(pid, signal.SIGTERM) + time.sleep(1) + os.kill(pid, signal.SIGKILL) + except Exception: + pass + + def _terminate_proc(self, p: Optional[subprocess.Popen]): if not p or p.poll() is not None: return @@ -328,5 +371,4 @@ class Deviceinfo(object): for p in candidates: if p.exists(): return p - raise FileNotFoundError(f"iproxy not found, tried: {[str(c) for c in candidates]}") - # endregion \ No newline at end of file + raise FileNotFoundError(f"iproxy not found, tried: {[str(c) for c in candidates]}") \ No newline at end of file diff --git a/Module/FlaskService.py b/Module/FlaskService.py index 85fdcf7..400b1c6 100644 --- a/Module/FlaskService.py +++ b/Module/FlaskService.py @@ -63,7 +63,6 @@ def start_socket_listener(): while True: try: conn, addr = s.accept() - LogManager.info(f"[INFO] Connection from {addr}") except Exception as e: LogManager.error(f"[ERROR] accept 失败: {e}") continue diff --git a/Utils/ControlUtils.py b/Utils/ControlUtils.py index 6a7abfa..7c2c915 100644 --- a/Utils/ControlUtils.py +++ b/Utils/ControlUtils.py @@ -1,4 +1,3 @@ -import math import random import re import time @@ -178,3 +177,30 @@ class ControlUtils(object): left_x = max(1, rect.x - 20) center_y = rect.y + rect.height // 2 session.tap(left_x, center_y) + + + + + # 点击一个随机范围 + @classmethod + def tap_mini_cluster(cls, center_x: int, center_y: int, session, points=5, duration_ms=60): + """ + 以 (center_x, center_y) 为中心,在 3×3 像素范围内摆 `points` 个邻近坐标, + 一次性同时按下,持续 `duration_ms` 后抬起。 + """ + # 1. 生成邻像素坐标(±1 像素内) + cluster = [] + for i in range(points): + dx = random.randint(-1, 1) + dy = random.randint(-1, 1) + cluster.append((center_x + dx, center_y + dy)) + + # 2. 随机持续时间 50–100 ms + duration_s = random.randint(50, 100) / 1000.0 + + # 3. 下发多点触控 + session.tap_hold(cluster, duration=duration_s) + + # 检测五分钟前和当前的状态是否相同 + # @classmethod + # def compareCurrentWithPreviousState(cls,xml):