From 27426f1f8fa5e69bd9fc47dcedaed63daedd9406 Mon Sep 17 00:00:00 2001 From: milk <53408947@qq.com> Date: Sat, 25 Oct 2025 00:22:16 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Module/DeviceInfo.py | 1047 +++++------------ Module/Main.py | 2 +- Module/__pycache__/DeviceInfo.cpython-312.pyc | Bin 51028 -> 24971 bytes .../__pycache__/FlaskService.cpython-312.pyc | Bin 33657 -> 34006 bytes .../FlaskSubprocessManager.cpython-312.pyc | Bin 13182 -> 13260 bytes Module/__pycache__/Main.cpython-312.pyc | Bin 3738 -> 3738 bytes Utils/AiUtils.py | 5 + Utils/LogManager.py | 2 +- Utils/__pycache__/AiUtils.cpython-312.pyc | Bin 50171 -> 50335 bytes Utils/__pycache__/LogManager.cpython-312.pyc | Bin 14663 -> 14663 bytes 10 files changed, 273 insertions(+), 783 deletions(-) diff --git a/Module/DeviceInfo.py b/Module/DeviceInfo.py index 00b4960..32516ff 100644 --- a/Module/DeviceInfo.py +++ b/Module/DeviceInfo.py @@ -1,926 +1,411 @@ # -*- coding: utf-8 -*- +""" +极简稳定版设备监督器(DeviceInfo):加详细 print 日志 + - 每个关键节点都会 print,便于人工观察执行到哪一步 + - 保留核心逻辑:监听上下线 / 启动 WDA / 起 iproxy / 通知前端 +""" import os -import signal -import subprocess -import threading import time -from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError -from pathlib import Path -from typing import Dict, Optional, List, Any -import random +import threading +import subprocess import socket -import http.client -import psutil -import hashlib # 保留扩展 +from pathlib import Path +from typing import Dict, Optional import platform - +import psutil +import http.client import tidevice import wda from tidevice import Usbmux, ConnectionType from tidevice._device import BaseDevice from Entity.DeviceModel import DeviceModel -from Entity.Variables import WdaAppBundleId, wdaFunctionPort, wdaScreenPort +from Entity.Variables import WdaAppBundleId, wdaScreenPort, wdaFunctionPort from Module.FlaskSubprocessManager import FlaskSubprocessManager from Module.IOSActivator import IOSActivator from Utils.LogManager import LogManager def _monotonic() -> float: - """统一用 monotonic 计时,避免系统时钟跳变影响定时/退避。""" return time.monotonic() +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("未找到可用端口") + class DeviceInfo: + # ---- 端口分配:加一个最小的“保留池”,避免并发选到同一个端口 ---- + 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("端口分配失败:没有可用端口") - # --- 时序参数(支持环境变量覆盖) --- - REMOVE_GRACE_SEC = float(os.getenv("REMOVE_GRACE_SEC", "8.0")) # 设备离线宽限期 - ADD_STABLE_SEC = float(os.getenv("ADD_STABLE_SEC", "2.5")) # 设备上线稳定期 - ORPHAN_COOLDOWN = float(os.getenv("ORPHAN_COOLDOWN", "8.0")) # 拓扑变更后暂停孤儿清理 - HEAL_INTERVAL = float(os.getenv("HEAL_INTERVAL", "5.0")) # 健康巡检间隔 + def _release_port(self, port: int): + with self._lock: + self._reserved_ports.discard(port) - # 端口策略(支持环境变量覆盖) - PORT_RAND_LOW_1 = int(os.getenv("PORT_RAND_LOW_1", "9111")) - PORT_RAND_HIGH_1 = int(os.getenv("PORT_RAND_HIGH_1", "9499")) - PORT_RAND_LOW_2 = int(os.getenv("PORT_RAND_LOW_2", "20000")) - PORT_RAND_HIGH_2 = int(os.getenv("PORT_RAND_HIGH_2", "48000")) - PORT_SCAN_START = int(os.getenv("PORT_SCAN_START", "49152")) - PORT_SCAN_LIMIT = int(os.getenv("PORT_SCAN_LIMIT", "10000")) - - # 自愈退避 - BACKOFF_MAX_SEC = float(os.getenv("BACKOFF_MAX_SEC", "15.0")) - BACKOFF_MIN_SEC = float(os.getenv("BACKOFF_MIN_SEC", "1.5")) - BACKOFF_GROWTH = float(os.getenv("BACKOFF_GROWTH", "1.7")) - - # WDA Ready 等待(HTTP 轮询方式,不触发 xctest) + ADD_STABLE_SEC = float(os.getenv("ADD_STABLE_SEC", "2.0")) + REMOVE_GRACE_SEC = float(os.getenv("REMOVE_GRACE_SEC", "6.0")) WDA_READY_TIMEOUT = float(os.getenv("WDA_READY_TIMEOUT", "35.0")) - # WDA 轻量复位策略 - MJPEG_BAD_THRESHOLD = int(os.getenv("MJPEG_BAD_THRESHOLD", "3")) # 连续几次 mjpeg 健康失败才重置 WDA - WDA_RESET_COOLDOWN = float(os.getenv("WDA_RESET_COOLDOWN", "10")) # WDA 复位冷却,避免风暴 - - # 防连坐参数(支持环境变量) - GLITCH_SUPPRESS_SEC = float(os.getenv("GLITCH_SUPPRESS_SEC", "6.0")) # 扫描异常后抑制移除的秒数 - MASS_DROP_RATIO = float(os.getenv("MASS_DROP_RATIO", "0.6")) # 一次性丢失占比阈值 - ABSENT_TICKS_BEFORE_REMOVE = int(os.getenv("ABSENT_TICKS_BEFORE_REMOVE", "3")) # 连续缺席轮数 - - def __init__(self): - # 自增端口游标仅作兜底扫描使用 - self._port = 9110 - self._models: Dict[str, DeviceModel] = {} - self._procs: Dict[str, subprocess.Popen] = {} - self._manager = FlaskSubprocessManager.get_instance() - 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] = {} # udid -> next_allowed_ts - - # 并发保护 & 状态表 + def __init__(self) -> None: self._lock = threading.RLock() - self._port_by_udid: Dict[str, int] = {} # UDID -> 当前使用的本地端口(映射 wdaScreenPort) - self._pid_by_udid: Dict[str, int] = {} # UDID -> iproxy PID + 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() + LogManager.info("DeviceInfo 初始化完成", udid="system") + print("[Init] DeviceInfo 初始化完成") - # 抗抖 - self._last_seen: Dict[str, float] = {} # udid -> ts - self._first_seen: Dict[str, float] = {} # udid -> ts(首次在线) - self._last_topology_change_ts = 0.0 - - # 短缓存:设备信任、WDA运行态(仅作节流) - self._trusted_cache: Dict[str, float] = {} # udid -> expire_ts - self._wda_ok_cache: Dict[str, float] = {} # udid -> expire_ts - - # 新增:MJPEG 连续坏计数 + 最近一次 WDA 复位时间 - self._mjpeg_bad_count: Dict[str, int] = {} - self._last_wda_reset: Dict[str, float] = {} - - # 新增:按 UDID 的 /status 探测单飞锁,避免临时 iproxy 并发 - self._probe_locks: Dict[str, threading.Lock] = {} - - # 防连坐 - self._scan_glitch_until = 0.0 # 截止到该时间前,认为扫描不可靠,跳过移除 - self._absent_ticks: Dict[str, int] = {} # udid -> 连续缺席次数 - - LogManager.info("DeviceInfo init 完成;日志已启用", udid="system") - - # ---------------- 主循环 ---------------- def listen(self): - method = "listen" - LogManager.method_info("进入主循环", method, udid="system") - orphan_gc_tick = 0 + LogManager.method_info("进入主循环", "listen", udid="system") + print("[Listen] 开始监听设备上下线...") while True: - now = _monotonic() try: usb = Usbmux().device_list() - online_now = {d.udid for d in usb if d.conn_type == ConnectionType.USB} + online = {d.udid for d in usb if d.conn_type == ConnectionType.USB} except Exception as e: LogManager.warning(f"[device_list] 异常:{e}", udid="system") + print(f"[Listen] 获取设备列表异常: {e}") time.sleep(1) continue - # 记录“看到”的时间戳 - for u in online_now: - if u not in self._first_seen: - self._first_seen[u] = now - LogManager.method_info("first seen", method, udid=u) + now = _monotonic() + for u in online: + self._first_seen.setdefault(u, now) self._last_seen[u] = now with self._lock: known = set(self._models.keys()) - # -------- 全局扫描异常检测(防连坐)-------- - missing = [u for u in known if u not in online_now] - mass_drop = (len(known) > 0) and ( - (len(online_now) == 0) or - (len(missing) / max(1, len(known)) >= self.MASS_DROP_RATIO) - ) - if mass_drop: - self._scan_glitch_until = now + self.GLITCH_SUPPRESS_SEC - LogManager.method_warning( - f"检测到扫描异常:known={len(known)}, online={len(online_now)}, " - f"missing={len(missing)},进入抑制窗口 {self.GLITCH_SUPPRESS_SEC}s", - method, udid="system" - ) - - # 真正移除(仅在非抑制窗口内 + 连续缺席达到阈值 才移除) - for udid in list(known): - if udid in online_now: - # 在线:清空缺席计数 - self._absent_ticks.pop(udid, None) - continue - - # 离线:记录一次缺席 - miss = self._absent_ticks.get(udid, 0) + 1 - self._absent_ticks[udid] = miss - - last = self._last_seen.get(udid, 0.0) - exceed_grace = (now - last) >= self.REMOVE_GRACE_SEC - exceed_ticks = miss >= self.ABSENT_TICKS_BEFORE_REMOVE - - # 抑制窗口内:跳过任何移除 - if now < self._scan_glitch_until: - continue - - if exceed_grace and exceed_ticks: - # --- 移除前的“可达性”反校验 --- + for udid in online - known: + if (now - self._first_seen.get(udid, now)) >= self.ADD_STABLE_SEC: + print(f"[Add] 检测到新设备: {udid}") try: - with self._lock: - model = self._models.get(udid) - port = model.screenPort if model else -1 - reachable = False - # 1) ip:port 的 MJPEG 是否还在 - if port and port > 0 and self._health_check_mjpeg(port, timeout=0.8): - reachable = True - # 2) WDA /status 是否仍然正常 - if not reachable and self._health_check_wda(udid): - reachable = True - - if reachable: - # 误报:续命 - self._last_seen[udid] = now - self._absent_ticks[udid] = 0 - LogManager.method_info("离线误报:反校验可达,取消移除并续命", method, udid=udid) - continue + self._add_device(udid) except Exception as e: - LogManager.method_warning(f"离线反校验异常:{e}", method, udid=udid) + LogManager.method_error(f"新增失败:{e}", "listen", udid=udid) + print(f"[Add] 新增失败 {udid}: {e}") - LogManager.info( - f"设备判定离线(超过宽限期 {self.REMOVE_GRACE_SEC}s 且 连续缺席 {self._absent_ticks[udid]} 次)", - udid=udid - ) - self._remove_device(udid) - self._last_topology_change_ts = now - # 清理计数 - self._absent_ticks.pop(udid, None) - - # 真正新增(连续在线超过稳定期) - 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: - LogManager.info(f"新增设备稳定上线:{to_add}", udid="system") - futures = {self._pool.submit(self._add_device, u): u for u in to_add} - try: - for f in as_completed(futures, timeout=45): - try: - f.result() - self._last_topology_change_ts = _monotonic() - except Exception as e: - LogManager.error(f"异步连接失败:{e}", udid="system") - except TimeoutError: - for fut, u in futures.items(): - if not fut.done(): - fut.cancel() - LogManager.method_warning("新增设备任务超时已取消", method, udid=u) - - # 定期健康检查 + 自愈 - self._check_and_heal_tunnels(interval=self.HEAL_INTERVAL) - - # 周期性孤儿清理(拓扑变更冷却之后) - orphan_gc_tick += 1 - if orphan_gc_tick >= 10: - orphan_gc_tick = 0 - if (_monotonic() - self._last_topology_change_ts) >= self.ORPHAN_COOLDOWN: - self._cleanup_orphan_iproxy() + for udid in list(known): + if udid in online: + continue + last = self._last_seen.get(udid, 0.0) + if (now - last) >= self.REMOVE_GRACE_SEC: + print(f"[Remove] 检测到设备离线: {udid}") + try: + self._remove_device(udid) + except Exception as e: + LogManager.method_error(f"移除失败:{e}", "listen", udid=udid) + print(f"[Remove] 移除失败 {udid}: {e}") time.sleep(1) - # ---------------- 新增设备 ---------------- def _add_device(self, udid: str): method = "_add_device" - LogManager.method_info("开始新增设备", method, udid=udid) + print(f"[Add] 开始新增设备 {udid}") if not self._trusted(udid): - LogManager.method_warning("未信任设备,跳过", method, udid=udid) + print(f"[Add] 未信任设备 {udid}, 跳过") return - # 获取系统主版本 try: dev = tidevice.Device(udid) - system_version_major = int(dev.product_version.split(".")[0]) - except Exception as e: - LogManager.method_warning(f"读取系统版本失败:{e}", method, udid=udid) - system_version_major = 0 # 保底 + major = int(dev.product_version.split(".")[0]) + except Exception: + major = 0 - # === iOS>17:被动探测 WDA;未运行则交给 IOSActivator,并通过 HTTP 轮询等待 === - if system_version_major > 17: - if self._wda_is_running(udid): - LogManager.method_info("检测到 WDA 已运行,直接映射", method, udid=udid) + if not self._wda_http_status_ok_once(udid): + if major > 17: + print(f"[WDA] iOS>17 调用 IOSActivator (port={wdaScreenPort})") + IOSActivator().activate(udid) else: - LogManager.method_info("WDA 未运行,调用 IOSActivator(pymobiledevice3 自动挂载)", method, udid=udid) - try: - ios = IOSActivator() - threading.Thread(target=ios.activate, args=(udid,), daemon=True).start() - except Exception as e: - LogManager.method_error(f"IOSActivator 启动异常:{e}", method, udid=udid) - return - # 关键:HTTP 轮询等待 WDA Ready(默认最多 35s),不会触发 xctest - if not self._wait_wda_ready_http(udid, total_timeout_sec=self.WDA_READY_TIMEOUT): - LogManager.method_error("WDA 未在超时内就绪(iOS>17 分支)", method, udid=udid) - return - else: - # iOS <= 17:保持原逻辑(app_start + 简单等待) - try: + print(f"[WDA] iOS<=17 启动 WDA app_start (port={wdaScreenPort})") dev = tidevice.Device(udid) - LogManager.method_info(f"app_start WDA: {WdaAppBundleId}", method, udid=udid) dev.app_start(WdaAppBundleId) - time.sleep(3) - except Exception as e: - LogManager.method_error(f"WDA 启动异常:{e}", method, udid=udid) + time.sleep(2) + if not self._wait_wda_ready_http(udid, self.WDA_READY_TIMEOUT): + print(f"[WDA] WDA 未在超时内就绪, 放弃新增 {udid}") return - # 获取屏幕信息 - w, h, s = self._screen_info(udid) - if w == 0 or h == 0 or s == 0: - LogManager.method_warning("未获取到屏幕信息,放弃新增", method, udid=udid) - return + print(f"[WDA] WDA 就绪,准备获取屏幕信息 {udid}") + # 给 WDA 一点稳定时间,避免刚 ready 就查询卡住 + time.sleep(0.5) + # 带超时的屏幕信息获取,避免卡死在 USBClient 调用里 + w, h, s = self._screen_info_with_timeout(udid, timeout=3.5) + if not (w and h and s): + # 再做几次快速重试(带超时) + for i in range(4): + print(f"[Screen] 第{i+1}次获取失败, 重试中... {udid}") + time.sleep(0.6) + w, h, s = self._screen_info_with_timeout(udid, timeout=3.5) + if w and h and s: + break - # 启动 iproxy(不复用端口:直接新端口) - proc = self._start_iproxy(udid, port=None) + if not (w and h and s): + print(f"[Screen] 屏幕信息仍为空,继续添加 {udid}") + + port = self._alloc_port() + print(f"[iproxy] 准备启动 iproxy 映射 {port}->{wdaScreenPort}") + proc = self._start_iproxy(udid, local_port=port) if not proc: - LogManager.method_error("启动 iproxy 失败,放弃新增", method, udid=udid) + self._release_port(port) + print(f"[iproxy] 启动失败,放弃新增 {udid}") return with self._lock: - port = self._port_by_udid[udid] model = DeviceModel(deviceId=udid, screenPort=port, width=w, height=h, scale=s, type=1) model.ready = True self._models[udid] = model - self._procs[udid] = proc - # 初始化计数 - self._mjpeg_bad_count[udid] = 0 + self._iproxy[udid] = proc + self._port_by_udid[udid] = port - LogManager.method_info(f"设备添加完成,port={port}, {w}x{h}@{s}", method, udid=udid) + print(f"[Manager] 准备发送设备数据到前端 {udid}") self._manager_send(model) + print(f"[Add] 设备添加成功 {udid}, port={port}, {w}x{h}@{s}") - # ---------------- 移除设备(修复:总是发送离线通知) ---------------- def _remove_device(self, udid: str): method = "_remove_device" - LogManager.method_info("开始移除设备", method, udid=udid) + print(f"[Remove] 正在移除设备 {udid}") with self._lock: model = self._models.pop(udid, None) - proc = self._procs.pop(udid, None) - pid = self._pid_by_udid.pop(udid, None) + proc = self._iproxy.pop(udid, None) self._port_by_udid.pop(udid, None) - # 清缓存,防止误判 - self._trusted_cache.pop(udid, None) - self._wda_ok_cache.pop(udid, None) - self._last_seen.pop(udid, None) self._first_seen.pop(udid, None) - self._mjpeg_bad_count.pop(udid, None) - self._last_wda_reset.pop(udid, None) - self._absent_ticks.pop(udid, None) + self._last_seen.pop(udid, None) self._kill(proc) - if pid: - self._kill_pid_gracefully(pid) if model is None: - # 构造一个最小的“离线模型”通知前端 - try: - offline = DeviceModel(deviceId=udid, screenPort=-1, width=0, height=0, scale=0.0, type=2) - offline.ready = False - self._manager_send(offline) - LogManager.method_info("设备移除完毕(无原模型,已发送离线通知)", method, udid=udid) - except Exception as e: - LogManager.method_warning(f"离线通知(构造模型)异常:{e}", method, udid=udid) - return - + model = DeviceModel(deviceId=udid, screenPort=-1, width=0, height=0, scale=0.0, type=2) model.type = 2 model.ready = False model.screenPort = -1 - try: - self._manager_send(model) - finally: - LogManager.method_info("设备移除完毕(已发送离线通知)", method, udid=udid) + self._manager_send(model) + print(f"[Remove] 设备移除完成 {udid}") - # ---------------- 工具函数 ---------------- def _trusted(self, udid: str) -> bool: - # 30s 短缓存,减少 IO - now = _monotonic() - exp = self._trusted_cache.get(udid, 0.0) - if exp > now: - return True try: BaseDevice(udid).get_value("DeviceName") - self._trusted_cache[udid] = now + 30.0 + print(f"[Trust] 设备 {udid} 已信任") return True except Exception: + print(f"[Trust] 设备 {udid} 未信任") return False - # ======= WDA 探测/等待(仅走 iproxy+HTTP,不触发 xctest) ======= - def _get_probe_lock(self, udid: str) -> threading.Lock: - with self._lock: - lk = self._probe_locks.get(udid) - if lk is None: - lk = threading.Lock() - self._probe_locks[udid] = lk - return lk - - def _wda_http_status_ok(self, udid: str, timeout_sec: float = 1.2) -> bool: - """起临时 iproxy 到 wdaFunctionPort,探测 /status。增加单飞锁与严格清理。""" - method = "_wda_http_status_ok" - lock = self._get_probe_lock(udid) - if not lock.acquire(timeout=3.0): - # 有并发探测在进行,避免同时起多个 iproxy;直接返回“未知→False” - LogManager.method_info("状态探测被并发锁抑制", method, udid=udid) - return False + def _wda_http_status_ok_once(self, udid: str, timeout_sec: float = 1.8) -> bool: + method = "_wda_http_status_ok_once" + tmp_port = self._alloc_port() + proc = None try: - tmp_port = self._pick_new_port() - proc = None - try: - cmd = [self._iproxy_path, "-u", udid, str(tmp_port), str(wdaFunctionPort)] - - # --- Windows 下隐藏 iproxy 控制台 --- - 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 - - proc = subprocess.Popen( - cmd, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - creationflags=creationflags, - startupinfo=startupinfo - ) - - if not self._wait_until_listening(tmp_port, initial_timeout=1.0): - LogManager.method_info(f"WDA探测:临时端口未监听({tmp_port})", method, udid=udid) - return False - - # /status 双重尝试,减少瞬态抖动 - for _ in (1, 2): - try: - conn = http.client.HTTPConnection("127.0.0.1", tmp_port, timeout=timeout_sec) - conn.request("GET", "/status") - resp = conn.getresponse() - _ = resp.read(256) - code = getattr(resp, "status", 0) - ok = 200 <= code < 400 - LogManager.method_info(f"WDA探测:/status code={code}, ok={ok}", method, udid=udid) - try: - conn.close() - except Exception: - pass - if ok: - return True - time.sleep(0.2) - except Exception as e: - LogManager.method_info(f"WDA探测异常:{e}", method, udid=udid) - time.sleep(0.2) + print(f"[WDA] 启动临时 iproxy 以检测 /status {udid}") + proc = self._spawn_iproxy(udid, local_port=tmp_port, remote_port=wdaScreenPort) + if not self._wait_until_listening(tmp_port, 3.0): + print(f"[WDA] 临时端口未监听 {tmp_port}") + self._release_port(tmp_port) return False - finally: - if proc: - try: - p = psutil.Process(proc.pid) - p.terminate() - try: - p.wait(timeout=1.2) - except psutil.TimeoutExpired: - p.kill() - p.wait(timeout=1.2) - except Exception: - # 兜底强杀 - try: - os.kill(proc.pid, signal.SIGTERM) - except Exception: - pass + for i in (1, 2): + try: + conn = http.client.HTTPConnection("127.0.0.1", tmp_port, timeout=timeout_sec) + conn.request("GET", "/status") + resp = conn.getresponse() + _ = resp.read(128) + code = getattr(resp, "status", 0) + ok = 200 <= code < 400 + print(f"[WDA] /status 第{i}次 code={code}, ok={ok}") + if ok: + return True + time.sleep(0.25) + except Exception as e: + print(f"[WDA] /status 异常({i}): {e}") + time.sleep(0.25) + return False finally: - try: - lock.release() - except Exception: - pass + if proc: + self._kill(proc) + # 无论成功失败,都释放临时端口占用 + self._release_port(tmp_port) - def _wait_wda_ready_http(self, udid: str, total_timeout_sec: float = None, interval_sec: float = 0.6) -> bool: - """通过 _wda_http_status_ok 轮询等待 WDA Ready。""" - method = "_wait_wda_ready_http" - if total_timeout_sec is None: - total_timeout_sec = self.WDA_READY_TIMEOUT + def _wait_wda_ready_http(self, udid: str, total_timeout_sec: float) -> bool: + print(f"[WDA] 等待 WDA Ready (超时 {total_timeout_sec}s) {udid}") deadline = _monotonic() + total_timeout_sec while _monotonic() < deadline: - if self._wda_http_status_ok(udid, timeout_sec=1.2): - LogManager.method_info("WDA 就绪(HTTP轮询)", method, udid=udid) + if self._wda_http_status_ok_once(udid): + print(f"[WDA] WDA 就绪 {udid}") return True - time.sleep(interval_sec) - LogManager.method_warning(f"WDA 等待超时(HTTP轮询,{total_timeout_sec}s)", method, udid=udid) + time.sleep(0.6) + print(f"[WDA] WDA 等待超时 {udid}") return False - def _wda_is_running(self, udid: str, cache_sec: float = 2.0) -> bool: - """轻量速查,走 HTTP /status(短缓存节流),避免触发 xctest。""" - now = _monotonic() - exp = self._wda_ok_cache.get(udid, 0.0) - if exp > now: - return True - ok = self._wda_http_status_ok(udid, timeout_sec=1.2) - if ok: - self._wda_ok_cache[udid] = now + cache_sec - return ok - def _screen_info(self, udid: str): - method = "_screen_info" try: + # 避免 c.home() 可能触发的阻塞,直接取 window_size c = wda.USBClient(udid, wdaFunctionPort) - c.home() size = c.window_size() - scale = c.scale - LogManager.method_info(f"屏幕信息:{int(size.width)}x{int(size.height)}@{float(scale)}", method, udid=udid) - return int(size.width), int(size.height), float(scale) + print(f"[Screen] 成功获取屏幕 {int(size.width)}x{int(size.height)} {udid}") + return int(size.width), int(size.height), float(c.scale) except Exception as e: - LogManager.method_warning(f"获取屏幕信息异常:{e}", method, udid=udid) - return 0, 0, 0 + print(f"[Screen] 获取屏幕信息异常: {e} {udid}") + return 0, 0, 0.0 - # ---------------- 端口/进程:不复用端口 ---------------- - def _is_port_free(self, 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 _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 _pick_new_port(self, tries: int = 40) -> int: - method = "_pick_new_port" - for _ in range(max(1, tries // 2)): - p = random.randint(self.PORT_RAND_LOW_1, self.PORT_RAND_HIGH_1) - if self._is_port_free(p): - LogManager.method_info(f"端口候选可用(首段):{p}", method, udid="system") - return p - else: - LogManager.method_info(f"端口候选占用(首段):{p}", method, udid="system") - for _ in range(tries): - p = random.randint(self.PORT_RAND_LOW_2, self.PORT_RAND_HIGH_2) - if self._is_port_free(p): - LogManager.method_info(f"端口候选可用(次段):{p}", method, udid="system") - return p - else: - LogManager.method_info(f"端口候选占用(次段):{p}", method, udid="system") - LogManager.method_warning("随机端口尝试耗尽,改顺序扫描", method, udid="system") - return self._pick_free_port(start=self.PORT_SCAN_START, limit=self.PORT_SCAN_LIMIT) + def _target(): + try: + result["val"] = self._screen_info(udid) + finally: + done.set() - def _wait_until_listening(self, port: int, initial_timeout: float = 2.0) -> bool: - """自适应等待端口监听:2s -> 3s -> 5s(最多约10s)。""" - method = "_wait_until_listening" - timeouts = [initial_timeout, 3.0, 5.0] - for to in timeouts: + 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): deadline = _monotonic() + to while _monotonic() < deadline: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.settimeout(0.25) if s.connect_ex(("127.0.0.1", port)) == 0: - LogManager.method_info(f"端口已开始监听:{port}", method, udid="system") + print(f"[Port] 端口 {port} 已监听") return True - time.sleep(0.06) - LogManager.method_info(f"监听验收阶段超时:{port},扩展等待", method, udid="system") - LogManager.method_warning(f"监听验收最终超时:{port}", method, udid="system") + time.sleep(0.05) + print(f"[Port] 端口 {port} 未监听") return False - def _start_iproxy(self, udid: str, port: Optional[int] = None) -> Optional[subprocess.Popen]: - method = "_start_iproxy" + 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)] try: - with self._lock: - old_pid = self._pid_by_udid.get(udid) - if old_pid: - LogManager.method_info(f"发现旧 iproxy,准备结束:pid={old_pid}", method, udid=udid) - self._kill_pid_gracefully(old_pid) - self._pid_by_udid.pop(udid, None) - - attempts = 0 - while attempts < 3: - attempts += 1 - local_port = port if (attempts == 1 and port is not None) else self._pick_new_port() - if not self._is_port_free(local_port): - LogManager.method_info(f"[attempt {attempts}] 端口竞争,换候选:{local_port}", method, udid=udid) - continue - - LogManager.method_info(f"[attempt {attempts}] 启动 iproxy,port={local_port}", method, udid=udid) - - 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(wdaScreenPort)] - try: - proc = subprocess.Popen( - cmd, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - creationflags=creationflags, - startupinfo=startupinfo - ) - except Exception as e: - LogManager.method_warning(f"创建 iproxy 进程失败:{e}", method, udid=udid) - continue - - if not self._wait_until_listening(local_port, initial_timeout=2.0): - LogManager.method_warning(f"[attempt {attempts}] iproxy 未监听,重试换端口", method, udid=udid) - self._kill(proc) - continue - - with self._lock: - self._procs[udid] = proc - self._pid_by_udid[udid] = proc.pid - self._port_by_udid[udid] = local_port - - LogManager.method_info(f"iproxy 启动成功并监听,pid={proc.pid}, port={local_port}", method, udid=udid) - return proc - - LogManager.method_error("iproxy 启动多次失败", method, udid=udid) - return None - + print(f"[iproxy] 启动进程 {cmd}") + return subprocess.Popen( + cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + creationflags=creationflags, + startupinfo=startupinfo, + ) except Exception as e: - LogManager.method_error(f"_start_iproxy 异常:{e}", method, udid=udid) + print(f"[iproxy] 创建进程失败: {e}") return None + 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}") + return None + print(f"[iproxy] 启动成功 port={local_port} {udid}") + return proc + def _kill(self, proc: Optional[subprocess.Popen]): - method = "_kill" if not proc: return try: p = psutil.Process(proc.pid) p.terminate() try: - p.wait(timeout=2.0) - LogManager.method_info("进程已正常终止", method, udid="system") + p.wait(timeout=1.5) except psutil.TimeoutExpired: - p.kill() - LogManager.method_warning("进程被强制杀死", method, udid="system") + p.kill(); p.wait(timeout=1.5) + print(f"[Proc] 已结束进程 PID={proc.pid}") except Exception as e: - LogManager.method_warning(f"结束进程异常:{e}", method, udid="system") + print(f"[Proc] 结束进程异常: {e}") - # ---------------- 自愈:直接换新端口重启 + 指数退避 ---------------- - def _next_backoff(self, prev_backoff: float) -> float: - if prev_backoff <= 0: - return self.BACKOFF_MIN_SEC - return min(prev_backoff * self.BACKOFF_GROWTH, self.BACKOFF_MAX_SEC) - - def _restart_iproxy(self, udid: str): - method = "_restart_iproxy" - now = _monotonic() - next_allowed = self._heal_backoff.get(udid, 0.0) - if now < next_allowed: - delta = round(next_allowed - now, 2) - LogManager.method_info(f"自愈被退避抑制,剩余 {delta}s", method, udid=udid) - return - - old_port = None - with self._lock: - proc = self._procs.get(udid) - if proc: - LogManager.method_info(f"为重启准备清理旧 iproxy,pid={proc.pid}", method, udid=udid) - self._kill(proc) - model = self._models.get(udid) - if not model: - LogManager.method_warning("模型不存在,取消自愈", method, udid=udid) - return - old_port = model.screenPort - - proc2 = self._start_iproxy(udid, port=None) - if not proc2: - prev = max(0.0, next_allowed - now) - backoff = self._next_backoff(prev) - self._heal_backoff[udid] = now + backoff - LogManager.method_warning(f"重启失败,扩展退避 {round(backoff,2)}s", method, udid=udid) - return - - # 成功后短退避(抑制频繁重启) - self._heal_backoff[udid] = now + 1.2 - - # 通知前端新端口 - with self._lock: - model = self._models.get(udid) - if model: - model.screenPort = self._port_by_udid.get(udid, model.screenPort) - self._models[udid] = model - self._manager_send(model) - LogManager.method_info(f"[PORT-SWITCH] {udid} {old_port} -> {self._port_by_udid.get(udid)}", method, udid=udid) - LogManager.method_info(f"重启成功,使用新端口 {self._port_by_udid.get(udid)}", method, udid=udid) - - # ---------------- 健康检查 ---------------- - def _health_check_mjpeg(self, port: int, timeout: float = 1.8) -> bool: - """使用 GET 真实探测 MJPEG:校验 Content-Type 和 boundary。尝试 /mjpeg -> /mjpegstream -> /""" - method = "_health_check_mjpeg" - paths = ["/mjpeg", "/mjpegstream", "/"] - for path in paths: - try: - conn = http.client.HTTPConnection("127.0.0.1", port, timeout=timeout) - conn.request("GET", path, headers={"Connection": "close"}) - resp = conn.getresponse() - ctype = (resp.getheader("Content-Type") or "").lower() - ok_hdr = (200 <= resp.status < 300) and ("multipart/x-mixed-replace" in ctype) - # 仅读少量字节,不阻塞 - chunk = resp.read(1024) - try: - conn.close() - except Exception: - pass - if ok_hdr and (b"--" in chunk): - return True - except Exception: - pass - return False - - def _health_check_wda(self, udid: str) -> bool: - """使用 HTTP 探测(带短缓存),避免触发 xctest。""" - # 加一次重试,减少瞬态波动 - if self._wda_is_running(udid, cache_sec=1.0): - return True - time.sleep(0.2) - return self._wda_is_running(udid, cache_sec=1.0) - - def _maybe_reset_wda_lightweight(self, udid: str) -> bool: - """在 MJPEG 多次异常但 /status 正常时,做 WDA 轻量复位。成功返回 True。""" - method = "_maybe_reset_wda_lightweight" - now = _monotonic() - last = self._last_wda_reset.get(udid, 0.0) - if now - last < self.WDA_RESET_COOLDOWN: - return False - - LogManager.method_warning("MJPEG 连续异常,尝试 WDA 轻量复位", method, udid=udid) - try: - dev = tidevice.Device(udid) - # 先尝试 stop/start - try: - dev.app_stop(WdaAppBundleId) - time.sleep(1.0) - except Exception: - pass - dev.app_start(WdaAppBundleId) - # 等待就绪(缩短等待) - if self._wait_wda_ready_http(udid, total_timeout_sec=12.0): - self._last_wda_reset[udid] = _monotonic() - return True - except Exception as e: - LogManager.method_warning(f"WDA stop/start 失败:{e}", method, udid=udid) - - # 兜底:iOS18+ 用 IOSActivator 再尝试 - try: - ios = IOSActivator() - ios.activate(udid) - if self._wait_wda_ready_http(udid, total_timeout_sec=12.0): - self._last_wda_reset[udid] = _monotonic() - return True - except Exception as e: - LogManager.method_warning(f"IOSActivator 复位失败:{e}", method, udid=udid) - - return False - - def _check_and_heal_tunnels(self, interval: float = 5.0): - method = "_check_and_heal_tunnels" - now = _monotonic() - if now - self._last_heal_check_ts < interval: - return - self._last_heal_check_ts = now - - if (now - self._last_topology_change_ts) < max(self.ORPHAN_COOLDOWN, 6.0): - LogManager.method_info("拓扑变更冷却中,本轮跳过自愈", method, udid="system") - return - - with self._lock: - items = list(self._models.items()) - - for udid, model in items: - port = model.screenPort - if port <= 0: - continue - - ok_local = self._health_check_mjpeg(port, timeout=1.8) - ok_wda = self._health_check_wda(udid) - LogManager.method_info(f"健康检查:mjpeg={ok_local}, wda={ok_wda}, port={port}", method, udid=udid) - - if ok_local and ok_wda: - self._mjpeg_bad_count[udid] = 0 - continue - - # 分层自愈:MJPEG 连续异常而 WDA 正常 → 优先复位 WDA - if (not ok_local) and ok_wda: - cnt = self._mjpeg_bad_count.get(udid, 0) + 1 - self._mjpeg_bad_count[udid] = cnt - if cnt >= self.MJPEG_BAD_THRESHOLD: - if self._maybe_reset_wda_lightweight(udid): - # 复位成功后重启 iproxy,确保新流映射 - self._restart_iproxy(udid) - self._mjpeg_bad_count[udid] = 0 - continue # 下一个设备 - # 若未达门槛或复位失败,仍执行 iproxy 重启 - LogManager.method_warning(f"检测到不健康,触发重启;port={port}", method, udid=udid) - self._restart_iproxy(udid) - continue - - # 其他情况(wda 不健康或两者都不健康):先重启 iproxy - LogManager.method_warning(f"检测到不健康,触发重启;port={port}", method, udid=udid) - self._restart_iproxy(udid) - - # ---------------- 进程枚举(结构化返回) ---------------- - def _get_all_iproxy_entries(self) -> List[Dict[str, Any]]: - """ - 返回结构化 iproxy 进程项: - { 'pid': int, 'name': str, 'cmdline': List[str], 'udid': str|None, 'local_port': int|None, 'remote_port': int|None } - """ - method = "_get_all_iproxy_entries" - entries: List[Dict[str, Any]] = [] - is_windows = os.name == "nt" - target_name = "iproxy.exe" if is_windows else "iproxy" - - for p in psutil.process_iter(attrs=["name", "cmdline", "pid"]): - try: - name = (p.info.get("name") or "").lower() - if name != target_name: - continue - cmdline = p.info.get("cmdline") or [] - if not cmdline: - continue - - udid = None - local_port = None - remote_port = None - - # 解析 -u 与后续的两个端口(LOCAL_PORT, REMOTE_PORT) - if "-u" in cmdline: - try: - i = cmdline.index("-u") - if i + 1 < len(cmdline): - udid = cmdline[i + 1] - # 在 -u udid 之后扫描数字端口 - ints = [] - for token in cmdline[i + 2:]: - if token.isdigit(): - ints.append(int(token)) - # 停止条件:拿到两个 - if len(ints) >= 2: - break - if len(ints) >= 2: - local_port, remote_port = ints[0], ints[1] - else: - # 兜底:全局找两个数字 - ints2 = [int(t) for t in cmdline if t.isdigit()] - if len(ints2) >= 2: - local_port, remote_port = ints2[-2], ints2[-1] - except Exception: - pass - - entries.append({ - "pid": p.info["pid"], - "name": name, - "cmdline": cmdline, - "udid": udid, - "local_port": local_port, - "remote_port": remote_port - }) - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue - - LogManager.method_info(f"扫描到候选 iproxy 进程数={len(entries)}", method, udid="system") - return entries - - # ---------------- 杀孤儿(含“同 UDID 的非当前实例”清理) ---------------- - def _cleanup_orphan_iproxy(self): - method = "_cleanup_orphan_iproxy" - - with self._lock: - live_udids = set(self._models.keys()) - live_pid_by_udid = dict(self._pid_by_udid) - live_port_by_udid = dict(self._port_by_udid) - - cleaned = 0 - for ent in self._get_all_iproxy_entries(): - pid = ent["pid"] - udid = ent.get("udid") - local_port = ent.get("local_port") - # 完全不认识的进程(无法解析 udid),跳过 - if not udid: - continue - - # 1) 完全孤儿:udid 不在活跃设备集,且 pid 不是任何已跟踪 pid → 杀 - if udid not in live_udids and pid not in live_pid_by_udid.values(): - self._kill_pid_gracefully(pid, silent=True) - cleaned += 1 - LogManager.method_info(f"孤儿 iproxy 已清理:udid={udid}, pid={pid}", method) - continue - - # 2) 同 UDID 的非当前实例:udid 活跃,但 pid != 当前 pid,且本地端口也不是当前端口 → 杀 - live_pid = live_pid_by_udid.get(udid) - live_port = live_port_by_udid.get(udid) - if udid in live_udids and pid != live_pid: - if (local_port is None) or (live_port is None) or (local_port != live_port): - self._kill_pid_gracefully(pid, silent=True) - cleaned += 1 - LogManager.method_info(f"清理同UDID旧实例:udid={udid}, pid={pid}, local_port={local_port}", method) - - if cleaned: - LogManager.method_info(f"孤儿清理完成,数量={cleaned}", method) - - # ---------------- 按 PID 强杀 ---------------- - def _kill_pid_gracefully(self, pid: int, silent: bool = False): - """优雅地结束进程,不弹出cmd窗口""" - try: - if platform.system() == "Windows": - # 不弹窗方式 - subprocess.run( - ["taskkill", "/PID", str(pid), "/F", "/T"], - stdout=subprocess.DEVNULL if silent else None, - stderr=subprocess.DEVNULL if silent else None, - creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000), - ) - else: - # Linux / macOS - os.kill(pid, signal.SIGTERM) - except Exception as e: - LogManager.method_error(f"结束进程 {pid} 失败: {e}", "_kill_pid_gracefully") - - # ---------------- 端口工具(兜底) ---------------- - def _pick_free_port(self, start: int = None, limit: int = 2000) -> int: - method = "_pick_free_port" - p = self._port if start is None else start - tried = 0 - while tried < limit: - p += 1 - tried += 1 - if self._is_port_free(p): - LogManager.method_info(f"顺序扫描找到端口:{p}", method, udid="system") - return p - LogManager.method_error("顺序扫描未找到可用端口(范围内)", method, udid="system") - raise RuntimeError("未找到可用端口(扫描范围内)") - - # ---------------- 其他 ---------------- def _manager_send(self, model: DeviceModel): - method = "_manager_send" try: self._manager.send(model.toDict()) - LogManager.method_info("已通知管理器(前端)", method, udid=model.deviceId) + print(f"[Manager] 已发送前端数据 {model.deviceId}") except Exception as e: - LogManager.method_warning(f"通知管理器异常:{e}", method, udid=model.deviceId) + print(f"[Manager] 发送异常: {e}") def _find_iproxy(self) -> str: - """优先环境变量 IPROXY_PATH;否则按平台在 resources/iproxy 查找。""" - method = "_find_iproxy" - env_path = os.getenv("IPROXY_PATH") if env_path and Path(env_path).is_file(): - LogManager.method_info(f"使用环境变量指定的 iproxy 路径:{env_path}", method, udid="system") + print(f"[iproxy] 使用环境变量路径 {env_path}") return env_path - base = Path(__file__).resolve().parent.parent - is_windows = os.name == "nt" - name = "iproxy.exe" if is_windows else "iproxy" + name = "iproxy.exe" if os.name == "nt" else "iproxy" path = base / "resources" / "iproxy" / name - LogManager.method_info(f"查找 iproxy 路径:{path}", method, udid="system") if path.is_file(): + print(f"[iproxy] 使用默认路径 {path}") return str(path) - - err = f"iproxy 不存在: {path}" - LogManager.method_error(err, method, udid="system") - raise FileNotFoundError(err) \ No newline at end of file + raise FileNotFoundError(f"iproxy 不存在: {path}") diff --git a/Module/Main.py b/Module/Main.py index eb10e2f..65777ce 100644 --- a/Module/Main.py +++ b/Module/Main.py @@ -49,7 +49,7 @@ def main(arg): if __name__ == "__main__": # 获取启动时候传递的参数 - main(sys.argv) + # main(sys.argv) # 添加iOS开发包到电脑上 deployer = DevDiskImageDeployer(verbose=True) diff --git a/Module/__pycache__/DeviceInfo.cpython-312.pyc b/Module/__pycache__/DeviceInfo.cpython-312.pyc index 1912666c2feb4f6c4bbe34105663170f1c5026a0..3b0eedca2468b56b0bf916416c4378697dfa2e98 100644 GIT binary patch literal 24971 zcmb__3wTpiy6E0{r%97EN!#=VDU?T`^g+w}*(dK7Xi+J0V(ku4C~e(IfcB({uQAm+ zZI5V+=ul^LrYcGtonw!9q&kk{oHO^ki3yV$JNJ0MAqCF;zB@(uE?zv}cklnNoxQUg zBFy=|v!rXUy_MPid2sJY`KX@{~8p;VErX991?cX>22FQyo<|sgI^KrI2??o2E%a>S~*` zb9JtxlOqwozj+fbXLm{1nBczb)ZbsA*xB#Ab0v;VotA zXOCZb_vDq)_oMH=e5KcW_1#aRuRn9;%;1$*e-S-B`qz)W)sADWEsibi2Ri=xaqnM0 zemVN=$kp?|ymH|u)=svy-DRB`dLw#qD8*`BVx2nQe|hY!=+p0CJN53>XP>z8#6Mko z@}Dn%{4!VbuOIha{^a82kA8ajqmQB=zH#+ePejk3o$CG7)z|u>USITuw=R#JoOia^wh{$^x~7(PF}eB$sl|gMim`6cX{mD%VW=7`RJmx*cu%;AANSzdVh5V zyj=awht}3kw&O$>yj(l^@|Cl1M0=mV^7i?ZYdGyX8y%7DY<3;8Ng}f9))rSpRp04q z?PzaqgB-;k=fg+cCnCD4j`ns(3+6X=bvmF;N@cUt!7T#3X}Hvm4#v@DlSXv+GtCv9 zot5r(rp>X1iKHH9nj2bJhogOG2kVOH;kDM?PP)Sk=pduEt=V~`!Tm6dw8i0c?r3gr zKImYfM7yQFp#rKNYj$5XgR#P@;1SHRVJ)Dcl1?P%|Cb+os(yh+`_8nEE$vwEU%XO%u{lQik zQ|p5;U3Z(~!8j-sb#_Hk>_ThS0&Vl)y#BsHng_pk3=AJpXdWjSmH>j0LPoMVib`cdMIpfhe~&~>c_fE2d0WbT4(Bo_o)_d`+9I?ivoPvDT)JD4ZQFM;xw*vZB_$rLLwiqG_nF+GcNP z++9<#Ba-5DxUfP;rz?`uP`}-dg-}V`P;cK|v!|h^qPlu_MD}oNI}?$&v~@Tg5oLWt z4a)*dOW7>=2%8O2gmyY{!K_v*n~CqK_Ex7IVZ?p_why}i%iz!X6hr_}DqVkSZ|bQ7 zL+yj@uOA4{Um2LcGN@X0vSM1Ilp7~h>V9pn_SAjh^!b7G`R5ejg=+!}*Np87sWxDl zqE|5>8(_SikScdVZ<Xd$Eukv~I_cxW4*7O}k%hM%l`a03AV!0QwXG zbhT*1x?-S1QHPTNEE0y~VXcB#TH2U7j=+m!6KYH=X%TEDCiuV8F+R5Ij z=iWqm`n31j%l%WYd=wq{>9yBBn(BQU2<5fMFGin!K6>UOpvpkkfRaYfe|+_s53c;& zH}wxc`^(8ErcRxo>U$ih+|`TET>0tw=+B>w_77isrvK_Me{$u_OIQ1zjGp-bI!}T9 zW!+i7yV1V8qHa@-eS7`=KpaIGo40J*Oe#fmqFksJ(Tm=()-?vE!XoOfyS5QxH!l9v zD{oEpeuCt?|NNCxqukVN+K3c5gNUTV8BrW`xE$@rB2uS|ji_r*v^WUO1%lSx&U74w ze84nvcsU4o0`st;w3%TD28d{PyW5dcari^QE`)EgHjK(4ipX7TtAmNq_6XhSR6q_+ zm&Yi2duMCQ5rR$#Vqw=og~jma{0^d%)U-+_Po31IotiUb8?^az3(vLq=DvL>Xj~E0 zt@KJK^~O_6hgJ=)^5+$OAoW$huMV142lZ>bGC&I|t+(az?t!YWl$jHH<3K}imsj?U zEbX#Z|6I+0Ww5YwWMIkp;E$;9a8~koT^h(3&Sysr~9+ z^+0}~c2Z**PP?SZnJ}53-8`KN4R0DKooS$KSQbpp`;L;4o`jY&tE-{(W(uXr^k)_Y zQ;Pkv;_q+TC~Z2TwbZXw+G!~~Z##`J?I$V>RRz><3)EF5(%%--5Vt_C5Y(~MjejRd z%0U6^Od;FskOJ!|0oGH-NJ&}-X}N@QDBw?dSfEluPZT2u1Q4V2{Dx^#-?fF3QQgrZ z<*qH%Ov)WCGz_r6Fgh);%2ZP-Fv>a@yB>1%v9tlw228`e(hkU&G@`X zc{Y(evyhh#HO;YJG9aDK%p)~29SZ2j0_=7Mt`7t6v0k#ERwk*F4f#3nM>{NztiuSu z2?2@Y0Tq)AEpmhwcdxBr@}PDuGwZH>N|@PDE03`{X2DA2LwYuh0#{`Yq^(^V+x$pM zoVjE1*K9$4fDcH22<`eA#6y&aI?R`dp38vsiBmGDlZ2KfW4n0T!2`*RJ_PI<{}5lV z8%71R2;@A5;(>rjOw>%;6sG}6!BoOX`PO{TK*a>gc8}IT3GZkva|v{71`TW{G)Hn+ zpjz=cd88g0-%pR?$Jf`Rc#J+qvlSk!lVok6tjr})Dvv;c*@emLA4Zr<2=P|H$|_nU zi7U*}*2LVtPN25Jyv6eYYU_lV-*r~WM|X^J%E*iNMAb%X*aUh9I=jie{u~tjo071`oSB(FiidWwW(KnXHaup zL~d^bj-OqIy`*wX`ompzH`B^Q6vvv|+zw|%Z)Y7&2YbxHka8!X&ulTYU`sG6#b`N3 zD=@?Er`kZf6(QG*0Q)V70XnIS~yJ0?(6pjlH=IhDJWwoe{@i zMWXy|*z#4tW+$nM)U^Jl-lppmt=>sbnlc7l;Y?c~(-zJw31pT8GnWQUWgoPR6@*vU z1y@7@%AkIgS2mfJG2nb5`#Vam z&hu7G7&3-*gSuhoNOjO)gZH%bq3prz>y$J#4`}{y_29aZU8796s4@Wm=2d>)Md=HiK*+*?X$cMBWe#AsR$AGxWcjF|(@;p$*o{&$p8 zohAB;g!_stY$*)DKU1NP_BFnzo-pU$l0(@vg%wIx-IBudrh+o$4ez?7pFNSDHPkeS z>y=iLY#^{QX(iugTK+*bt%vUc$D+^i=TrxEHGWmit;M)(+*;qhWq?tV$=<%D1XR!*DjEMzR+A-F8|&9+)b4Hi)?vqf%1z5^4e1M7v=KWb?V(G8R>Onc1W`oEv^rawS;i(|H)AFuUMG%$hvih*L2VAuHF3~v5ks@M1o<`RQix{2 zbMb`M5Y}1)T5DLlFrZyHY7S|O5ORxe0kjw6&rp2Zi4^1c+@;cA%hxC)I*`;ss;{Wr zUSn^lsp^(4Eh>o^cGv8vzpuu=X?I0cEPFX*r-6zM#QTcsz4pc}J8J6pGE`_2%_G!d=UVPfYkZCxJC^|tmAWCv-#9vE1!x#QBizOU^akRn#-3lvN(`+ zQ8sy<8+9ac2(U)IuSS3MT(s|{=(~MW-T~0jbU9s)qi=#r6Vb3cAFA)$(%$OYZ~gIB zHe*EXI>b7fLC887k?-D)NF5gt9B;VCA@Kc+dDxC%c7bfB5MV;azsuP z$u^p73@7fkm`+G#hhAG*6du}% z^5x{Qq}VG9sj??+g>R>0DtCg*u}l;eK?VJ!r6_D!6R@laTGqjP>O`(JoVz5DyCj%P zT4qhEjQ#pvz28zkw(^pydQz3@*OgvUEhUQ085;mWj%!?9h7}wbF0D4#eX!vF<@MS%qyQ!x7B`uOeREuOkQ2!nY=w6bjEnNbT9~OX^ z?~S64EQA%N`|_b}5&`cYBz;9Psz(ZXAYpq5BLFQ9@O?{Sd|z(7Pd;^6*eZMu=ynvK z$B7{W;5|X(bl4g%BdqYfb<@MGs!zDJpe z?>*ATl&BGdnbpJ0RAhw7=C(MYYk2iQZGLXp2aF53vV?HtL9*f{B1lksRA;DHXDW`V z!$>X@f=9)VoTmDA0XJU8SMUfLD;W8GWaO%tmQ9s>jSnZC0h7wlphpdJSI6d&M3jxq zV})oQx}@_!ZZILtn8yP!4?&(ejAn*}04f7vrvO*|67u0Kk{-!%pv^9%!X$uhZgY=J zM3BsUN&LwuJThEqvL^*76wN@%mB+vLw~uTua1K4PM`bX=t6b_h&1(r0c?hVpjOX-c z%wWZg86aDo5SWzM3?v@akKh#z{tAqi)DsaM(fHc$9N&L(6#9FP!f?+~G$xM1mR9FR znsN2wndsARTpqg+{p2lBd`1*)tw0Ie+1*gaHb6vVn|<3!?tW|Z}964yi2J9PE6QLf@@D~kw#(Cd8_ z471-l^^22Jzj+Q+qEj#VxcaNDWaRK3{rS%XNz0AGa;%A{#M&)cZM_0TB)F~c*tgqp zwBwjV)OE~C`^yVp295Q8MXL) z5shd$BI*_}huU3W31yMFib(e~RI-g&7m-UudAylz2WEoBxkTj7Hix5=*aF$Tn1Qw; z#&Mw8-R5GOun{sF99MBamdlPfx||Uya1U%9zJoZ&Fm~?y5p8UE4m2G_Wccf_fOV;e z4j5%zk61-p3gH_fQn&Nr2n`ZxM|)dqyCXt7BGPuS#L(`D{0Jo4$+)2-;9Z8b6t4j?!d;~!P4Es`Df>!neWeP z7*Ie2!ka1Yg+g!DM0(D5AdT4u=m~>4Y_J9l*05o5z_56NjlugWAI3OF+59&wg zuMBghL3*=+Im?*ANt1MH>7*rRxccn&)7u3mCTJ-ckb^9cS_+!$tl1;!r*$A7rdH4c zvI%qMa6veGQ6PI!IJ+p2T{P<* zUOz37rB~1smfWyqVZZ{i#wAP9v>JQ7X{569hWDJ&d`G3ER^taunZwHm*Mp!rk(o2} z*x+N~%te9BMWf|DQ!sN`Yyg8>N6N!8+J2=rTfw+=kgx8r&}P3cYl_d5lZ-AI2wY03qiIs6)i?66@@z%a)@ zZ|&Hwu`2)EjpHXS8Fo$@(*2e-V-;g%{)`PF!^TO2c_?KtWw`WI>YojkAEwpN_V%p| zSTc~DQcHgT(KdG;{4OxLwfJXMhAmZJS*l1c)%0!W5rD{F zf?!O2Dw$KeM)v7E9mZu6$oXtt!Dh4c^CfAU^Ce%TrEMvYe6dCj`M;;so71GfSFhWw zm42y}L;6dzeDf^jm-*@~`SLFp6>iCu{~=cn=|2>xw-(6%u%vM7Z27;;mP4BCn;Bv@ zGL3}I{fe-;3=J$`j0l6dP~xFyGAzJiVC{qu7p$EIQEMr-W@N8Q<7BoA8DN1mK8#o@ zA;kLv)vdr7g8?xK%bUzLA7{521q$Z;x4~pdB{f+JN&)A2kBm_s0mQ~G5zRssCz&uC zwRkphQzOrC9u^p!__X3PqN(=XqMlRk((~WEMzAtC&4l1Wp_2rD{ve~GR|g4)XYr}{ zlhMR$!b;`GOZXHTk&`j$JR|AUi{_?Hgq$vsQJYCkEWO9rbTh^#2n77dfCZ-+{W$|a zk_Q1sd*nTe9;HXlg4&stc$Cg2*8;vilg9T4O!RhSqD3P&dQ^-FjpOiZo5?su%+)={ zna=kMA`f-=KQ&Ge&T2ghh_}F4>Avj%D>R5j@$u_KW6VfgKls{R#GoFvsBNZ44KuTd zVeeJZ456oF0t4+)pUdLcwR;q?lR9`0qt~60> z6wN3vaYoP&>bDcZW1@W}YCQ{EGqcHWVfDx%o$paVJjbI1jGGc)LsU$<1c4re9|b5c z6+N=O)PSV#@Q(=|yh*|~w~lRx5xH@#I1ZI#qd5+iV?M2J!eMeO?<;RzzWCbZ3m5nz z)PodSuYUMGcqiPZi-s8;A;5c9v`@UHhq94$@+qBJ% z@VSq#zIGkd7rFd3cXrxA=ft{V4X&Zk=0@<~nxI=CI4DG48NK?CPfrc~WArCaM}6;I zx$st@b?Ve7(T|_x=3{lEARceR)d44r=rcb7Ur3JgkG|I*{ihdUmQ#1qMdkC;#SGa5`8lk>rF*O#c}B1?wI0? z`#Q>xw=%9n5yc?~cs_wo2WY$79N=dNvO^=c!ZBw?VQBq>*G@jpfy~s4zNzQmML!ba zv1t{7UaSI{BB(y2&z`k*D+{dzM7kw~)(Cy#Hoeg{i-<6wd~~sH5D1xw%GDZ^BorJ^ zPpnZ9J@{QR?iQE*n1gkKmnRIgv#r&|$ukx^IBePvxm*OWTyCemJq9?O zd2B0IO6B01SH|wZ-hsuSSmbTMT?YGPQG{Y2z^DduQ+c&3C~4ano0c;(psW^AaH3R1 z3JVgEA8kGi0u>FC(IIv|7SpZ78UZev;F1dEL=}tx9G8%I841xsf}u^2*j|S+=Ql)9 znn;`FRiJ2Ou#FasX83g_Ayw(OdQ*Q-Z_jXJNIz#%o8~uz6fip9r}7(Dg|y%fW1N=B zz{FUKz+OPoNtuhCaKHs|C0WA%RB_K<1^ zcu$7)`2l_YNP(~1ug?$aS9)b%>rKS`G%KK;HS7v$3rNqa#tOzV{KkzTZ3X%DJbTnURxsX-YrnV2-~4dM+~Tc)DR^u8woT;E_g43B@7+FZhW;k<=ZW7IEcRB1 zwcLjS+Jaje=niCOwQj~@PpGtERdzs?J-jTW%8vu$=y4QcIk{>)ZM<}R?)a|pN5)(H zx~-Q~+rGc)peaKZculD@Cd}#2)qvlWI^*`O-86h;8tk}*x1ET=-(R~;L;XQoq1cuu z{a1}-hP$T$&mB!qi?kF-PVnh@^YE|J%QaU-9R5FQips*g!e zrimO?;NSnw-A=Hit+LyTAAeqhuPASA{f@F_B3kjUYhLMW36kRfqAYzge zMS%nX2b%6^As4sUOLZxD$-<_r;~fg?nvXh&`g>m^GG_a&F{pLp=CVdV{52;zG;;62 zu5<6THW`bEE22i*GI28EKo#}QyblF3H)0H{(4p_047P0ON`8V>*g&-#M05P=i>p^KOyd8V?$iNUHhhJFe zC8>T*_$~SNh?hOG_%8^42eBT&?VUS*t zgL>#5kPzao`ULCWqo*zfqXd1+7ocxZz$yqdBAG!t*>M5D+;HsRoLZQ@(xXh~fXSqQ zE-OZRVO+{&IyvDtQF&C!^kKmwSiv_~sigAB3;nI0|7UIbx(`)G-2Q~RhE%&G;jmmnoJz5Z9tLcn~MI`##a zj7X=ziQt#VJ^&R2e?al_g*P}mp|uz^7$9g`-;}UVLA!_sjax3x2`r{wP;KN0Inje~ z{cygUfGs)8rnMGOBJv4@_}^BSE?rSn0{@k=C$UfIrkciXW$a6A6=o^8&vKfXFCnLO z0I-ld4N{yYgdZ2^F5HN&YB5yp4okNf?mc|p@Y zU~*7AT@o-Y89g*64Vl&w!L;1>fNv{j=OQ*mw1gY1GIOsNfB+4-#QIepGFZPw zKSCm3ow%f3IBlacz>=m*oi{*FOH$M3P3Fu#yXN$oaL%$o&NAPT@s<8P5BYPJ1#_AQ zs$<;PNcyNcWL`RvGdrBKIFPeAn6m`REV*aZr`02K&*%m!CUcPeviM4aIm@6ZD|bZp zr>sRI&QR8(fl6ZkTQb@_+Tc^81Ucp&-#uRK&)yO;Z~ZzmYhw0X5Mb{%=9?x8{8qOP zZXI@AG8ar|!r-T8QRb}w2~z5hPW1bmX_N)rW+gC1w04&I@_i;>)fnvqVR}u-ymk^; zP0}YYn_rm=&={9C?}zItcOA-Y=N15jmnt$h(bQ)qImTI)s#=-k^L!d!ey2=_^zU*d zkp4oZu3azxB6WW4D)|>>3d~t0$DH+2NdKOmziELqlIH4gfyVB~xC?ya2V@wi^hN6%|#~@(^LjAPQe9GHrjs~K(Y@|_v=?(z*65>^txms0Rb z*Yy5u z*kj{qpO=lhe*gEM{(Z2j!C%?vZ`=c(9{xT1{Kf}E+Wq7N$x!!T_X`_duNsw|*Sx6- z7L7O>e-|jb6`?b})44FqhumUN{SpHb`*mA$IGNi2nPZgvk>b%=d^lp5j zGQYBb`V7uzs-&MSNvo1de=C(kntdD!XNZylvcC)^&rr|M&nTWzqDToho&W+zwN@+r ztWpB1sAPMQ-(Sxvyb7<^>%Og+cs2yRv_m#fwiI_QBvAeJ(ZwsH&s}?_FJ?Rxg8~@E zI0Sw5;;HDFpIJ++(UTuT=C$EpPrdW==<}~ly>kK1PZI#Hi%3DUz#{wG5}PD>;N(s4 zcLQnRi!wxPy^>^7dat16>xC<3)21uCQq6dQlrMz(nh>k zg&Rgh!8C(l)9#)FeclDdaKGI`_H&%Kg53j;h`i<)GX9E2a&`d>c4+gB$k4<_kcv$e zUj?#HNHg{lwxwNc5T0xZB69rYp@)$8ZrsR`-yz8EF8EtkXaz3FBT)T^lhj{8lV{8v z&he+s1CCuk%iGoeSnp#&z11tb1vadlX)TqS=iL(4qGEAmldn3YT?Oa9xyvQ`)IgN` zyL!8ZGei1(f)tkfj{922oN-IcgbA*xNFQ+Y_V`t~9N9%O%LgQv_ra^x2Y)a%)Q6qUXg2goZpTJ@ODEJLyA zBwr&)UMh9Iw{kKw{{_#;!-33&-p!N7+!qQ*EJ34fGCgN#|KR@Dm;Y-0YwJH)9xkg1 zl+^^&Yo{q$TEUn3V5>tbz=%0yoKcM-9S+gzL`oqaz`v7qitFaqq}JYo_Ihv z>>55WvemCE4yj6jDZZtLmSiBeoyhZiYFb%klK<8uugX;ujSl+`=*bDDQy{9e1V#4g#H^ zgf<{P3SUfi(9fe3%>g;DgmCv#P%6tosT`Xj;n_i*%LU-sE#MrO&cDN{jD!~;0_`?u6p*Wcd?U8*7$KD*|A`_A3xgp>0_~oTobQgv+QH7lehk$CsR);wMKo}}ba&&P zom=W^>){COak%^pK4B)gwRX5Is$p~e{oLe0SZFxZar}NFF|dCJJ+VtMdI z>ry)J0ouFca#~I}Z9yPyK`_nct(?&6`nNv2^%d~6Ulo9V+EveQoyeQzt?a9t$jXwPnt4`7-G5YL^S`| z>GM8Acu5v5#)bB6LQ>I;6ETE7QhZAiE|0(XdV%PZ-NxO zf8$8v4!?2Y?I~Q%Vk@PgHirXisUXB~(+wh^Z`_V-W@$|ZzO z&hP9YOd(Zee+khHBxfuP+5RRFX_4n3>?xQ@8=>?JNe zF<1j1Ex)leq+JTSWm4G|(A!2A`C3AHaN6RuM_@D0CDoVu%6(9sICPmZiLz?(=+@D- zzDLH)V+y}+LrAp|I`L`uY?xvnJqkX6Wnpc3KwJI;dYmo0?OYARe7d+Yhx(1N!b*Lrv_SGx>#|CV^s{sc zq(94~F>R4zI!9f(7H%NaRj!bJwn7eR;(C*OTlt}5RA+z^A5w`Snac=(x`008^?C5* zH<_5*0&b5^rrMtgcw~E|@I4Yh72Qh+rV7vpiMcrfsFFt|1W@5A0#s?8+X@}+i~ARU z`0FTfF6O+6Ip<=l8@W7DC9VcJ?g@O1f_)H*@g5quxKiQjz#C#YIp`;l&jFv5V>l3q zIJiSpC4k+_@G=wS#_mA?FOYVo7r_r*tWcjKQ!ho-xubVS%+}^U>pAT?^C;ZeL7<}C zx97vZd++c3<<)*;%_VIur$eOGOGzo*0yA0#^gpaE4QNXV%q)#*4=d;j8TF}5T`^Dk zX{xTmD*e=cDfogT@s$h(>gg6M|1f2^Y2=Xl>&Z``~y_UhH->$U8gQj={$} zn^{oYvHuRW2`?W>tA+Y?9j@9AxG23+fky^+f@FQCPn$pkD?q(X;c<9zT72wBY5( zdOg0)UKy}514qyqdNVqpy8`+$$b-9&*Y>XMTTea!KHfes*{=v`R}yo<3#Jf<<5ndH}PQsybUZRK(S^TwFgmjMK%f!?MKH35aC;ntl zQPZ*vc`ZHMaD&3JTmtN1&`}0DkEoj)$r1@t4^=-&s*km2Xod1)7PJ@`#Ljs zc<=D;vwKhP^)Fa8wk()e5zMUg%gkR}@`hW7A3oc9y44T20N)p!RUNd{_+{xgWEy$d zciD^N+0zfu>*U+%5$O#IpVN=hM)|UlvKtgUrWY#ZS;Gy}6g);6Z{Xv)akktx3R&7%A;3LfL>c#;4TeIf|=1g0(@@2ydM^=aDmIdHnHjEH*rgP=;r6X%@Q1F=6 zY2@pMTW-Pt)^R@;{!RdYsDQLdTCD&+ttT%)+6Kl2I|J9Q**V}Mio!kkqY{8Xok2v2 zTeq$CVV0l@1)KuHYXXq;un0O4RcBkX3+^2_%KjgG)u4+{5$EB<{v+1v$LJ3jDKJ73 z6EW~ddWvFa|2Y>?V-2_txw?utjt}lCcW{2n5ksutzGk+y`QbK)lRKva2%X~WlX*}X z_EAytJK4F*jZCpR;uC7zNg%@>;Afph@oU=2y$al}Ltca1j%0J`0LZyZ@77@f*#R?m z#ckDEZd0#k;r|Cf!9U7*3L@YLY5KZMM$2xbP;|;Sl;#`C@C}vr4VC(5%JOei{x_5! z(%(=7ySk(=DyR~+=oL=`Gg)sq-{|}xZ-&_Cy literal 51028 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 SGcyB|&Ib_d1IOgxW;FnCZF{Bw delta 333 zcmccC$@H_0iT5-wFBbz4)ENHCkebLV$@GMMqsDr6UX7H{Yz+p6BC%A>ti_WBrGzEJ zQo>UtQUupTtY!geU|?WmsAQ~U)QsHh!r>RsST%Wa`Z{h=Mg|6TCI*IL9>&RudcvFI zGafTCN^Ew{I>g8*HJL4EE0Eli&c%ti7b zjv|Oq0uew%8H$uatfL^7%4Gk1b4Inv?fDvv+M74zuVrM^+Z8d-Qe&{Wt2mQQ z(iF(P#gtc2lnT;Q3L^4Ag!yEisz}D5&1qGq7$+OnngW^WwIWRNAm~>de$2A@Wc^W=$s1c#0e(DCp8x;= diff --git a/Module/__pycache__/FlaskSubprocessManager.cpython-312.pyc b/Module/__pycache__/FlaskSubprocessManager.cpython-312.pyc index fbcc8cb3890c607f95527e8cb534c61073af8837..f993302b15e53a0f9c441cf51fad534d3fecc720 100644 GIT binary patch delta 527 zcmeyDb|#(gG%qg~0}#~B`ki5^u#vBVS;W-UDkeEUB{e3XvLquvFDBDJ*m3fDW*^}g zpt>Io3^#ZM`Xf6dKQb_L>P_Zk(GdaCA2=8Ug!^kdYcFufeCGrTZ1!YfWM?#;?9FY@ zd09s169b!|1LNdg?jja2gNet6w8 zBc;gTN4GcGU1^OeqBPai4))8Ug6aT=$ARydd+gW>o zL*_dtP++qq3nM$D=45Mbd**90I+L5Zi|Z7|uEN1mBM#&B~y&r?ob zM&o{K>!jPbf)=taTM&8y}9vdDQdA{l=}SbVz8 zB%2!oV&5UQOg^XVDtt}WW`gSt5y|OZ6TPPTd|+mfvz@H1q9=S?Qf`9d4H41lZWG<6 zdH}_xnm$aEqb3JysxUK%DNL@?RAU9w6QVY+&}3#} z^qRazTZ;J#uifNx+G@ZsvcJx2cZt_-Goy|!Bcsz~eO*PNYl5LS#HGJ8Bl#dpmzRz4 J*5nG~5CC8ti3|V$ diff --git a/Module/__pycache__/Main.cpython-312.pyc b/Module/__pycache__/Main.cpython-312.pyc index 24cd69eac3ec15f66b9916510cd494ebbed61053..9c0f17fa0645db6fab6cd17d3b5939865588e9fa 100644 GIT binary patch delta 22 ccmbOwJ4=@LG%qg~0}!m3^*f__BX2Js07&HqY06}jB<^TWy diff --git a/Utils/AiUtils.py b/Utils/AiUtils.py index 56fca3e..f62a65c 100644 --- a/Utils/AiUtils.py +++ b/Utils/AiUtils.py @@ -842,9 +842,14 @@ class AiUtils(object): return not (ny2 < ry1 or ny1 > ry2) # 任意重叠即算属于资料区 def is_toolbar_like(o) -> bool: + # 在 Cell 里的元素绝不是底部工具条(避免误杀“hgh”这类贴着底部的最后一条消息) + if get_ancestor_cell(o) is not None: + return False + txt = get_text(o) if txt in EXCLUDES_LITERAL: return True + y = cls.parse_float(o, 'y', 0.0) h = cls.parse_float(o, 'height', 0.0) near_bottom = (area_bot - (y + h)) < 48 diff --git a/Utils/LogManager.py b/Utils/LogManager.py index 3981133..0c8bb94 100644 --- a/Utils/LogManager.py +++ b/Utils/LogManager.py @@ -26,7 +26,7 @@ def _force_utf8_everywhere(): except Exception: pass -_force_utf8_everywhere() +# _force_utf8_everywhere() class LogManager: """ diff --git a/Utils/__pycache__/AiUtils.cpython-312.pyc b/Utils/__pycache__/AiUtils.cpython-312.pyc index 9ec2d69f9a5e56f8888893dafae06cd09a9433db..5454b6e1312a84567b068a90e59f9ed4ce663366 100644 GIT binary patch delta 2980 zcmbVOeNa@_6~Fhrx68g|-+q5DEUXK=AmA23ZTSia>PUKgLNd&}^6jMoF+Awv@;AHH_w4IWmW>ameO{UXM|HwjUNYy4iZ*gI2rvLQK zaL>Kx>z?yF=iKF94}B&~Yk$LWY6PG2?7gNve|Ss#DQiw&S5UjBov*L0KJau?dt2?^ zs;y;`OWmx+S<;L4*C-XKn?;zb#U$NT%*Y$5-sPj1yEt%HI-ac8;k#royU6eaxcwXY z@(`Lu3X?iwG?CA;*DGddIFL0tKcmU_=cH(kU!!`CMW_e$cu8`>JDT2%5Ng1@qFdFQ zPX1Wnp(%kPxX|IE_yh-dE zSh1eGR1&}jIB#f8dnlMR2qm+D=2IaQV<;V(zynQ?^^jJ2D64_umw}GyA&D2#L7@!r zq2RqBH$yrWDE)pxu~5dYw0I(-Rj^i|opk`QEtaA!E?G!XFbj*EAUn&` znyXug?T8$kU~Bf0p%N1*O;@oF!Ex9kIGWRAE%{!lb+2YArXaNBLs1w0A5m}rX4L;H z=w}v#?kaPAhvqX6DiDeTAUXvL06e?^Fm%j+0Rb*!sdpNd@E|i~E}e~EzBCh}OKv6F z#%qagt+&etlfO8-VzS^8lKHHIi3rKFBf158;UuOAZXrc*%Kabfewxn~Q`;O5xS-oo z0R^J@9N=nYFEM+M;1N<az@P7(7GtaxV5t=k<0IA`>4KkQ8h}>JYP9fIT}2> z`G&n>pyRFPNyATDUolMCEBX|$L>z8-a}D|AR8>#~G|GXoWU4O2c6s{2@u>G=;Elk9 zXJYH5cY67HxRu{>H7*?NJl{3cHQq7do#>qQmmfmXBQ-OLIECcyI;WbptOehklYG1@ zlLjg&>vkz{GO6cxf~Y!m-=bh38BDSPEM(02-$qJQ6bjB&G?*pALVsqV=?l+B4jx+Gn)Tsxn zLuhcz`Rzm7NAt$%vvmOQyDk0FV^5hezDahrR^U=H+*+gbz|4GokKAqjt83cR_yxk& z^ntREt#I4Rt+O<;*`qRx+;4l_Jnde0!?@N4JPw(Y-5{!#9faR=GkzK@+Y?0|}} zb(q|0FQ#EblJ+$lI5vAEZ5{!ck(X#HIl0fRj;fH=Ig2!08u|IYW-ria5;EIQ7W5ZH zDax=Fe@-)IZKwizEUve&zj|mvdXm z?VxYjcv_7Iy^!2#w{VWM^ZST^YTG%r1t1` zRScp8E}y(}w2B5QDUV&Faiw(g_(cV-myVvOWby0Ljn{jzlE_vPX&5}HI^8~tl-x#4 z{xZ1IIoA47Qq>SYxO>zv(J@(hscT@%AU?HiDyfPDhB6$o-kcU`?E?uT=4+fUVojb! z39|9tp)%)K^G8XW9vGKRIxkhljW?02Lm5m=FFKU0ikR&%-mH)4hgZ9SMzzS{d^PX% zsy=flA>#C$$vKsCIw1F=o74>#LMyt-Ys0CkhzS~nV$;ava1!90CjEK%K34uoPUS^% zXT+sHTXMs=a$xt%d3_xxR!?iIP%p=)OzZF1YC_pg+7&m>(-G=F0Fy z?k&CfB-_spqz&S6#l-IG`cfzuNMHu^wvcu{C+?ffUC4v~cgwJ}{LN1>wo1B-g)~l) z9)0@?gYQb*yT8IXU%D}Ijl$oP#x7wBw@7bZuA&{!Fbd0P`7Dy@=QL6pqLjRiwn>&N b-4y%^yz>5L({2mighLJgM6pe<2paW2l~62y delta 2945 zcmbVOdr(x@89(RV-OIl3zTYe?uU#H1;i05}2m&@z3xZOdiW=&brwHs)o8Y}cTOD^P zQ2y!5F3mHBI`d(zC}%Hrlm-yW~8d7!;zUt3p8 zM|0I4Usr!mZ}Z+3U%#iLe@}OFM~|+UnhtYyO7bgmITIhzR8wBh;GB& z+x($KH4Sg0mvc&W+pv?5!Q+D<3dB0uyeTEA9-8~s$UAg zQGm3?KU8ppAGAV7d{{D+fojW5X!4Yl;z*YC8qun?CP*T6iAgN_+ES-=yQaguzWjdLGZz- zR-(Z%5CPASE8q&a2XLYpV;5$`D`x0`XE!nA9&$0Je*IE(0JIW*m*4FN{;XCJV?M0l zlx=`h38+de@hGdnj#7+@#w;sYIxZDZVKy>XB%GR6W2uQ%q30{~BF4(9V@actqbxlk ztqm}VXCWFdbE2*?qg)f;sNakRX*-IRNkp8s--0UiiD;_K-I4wj&UB12vz26$F-o03 z1DA;tmr3?hS{eTAR)R^vI{K$p(r~204k9KMuNrWiMqDHNSqg0~Hw>o5;czC_gd^UC zBR0n(Hpf?%B4!QjvcwCA4OoX*k#H9Lu49+E^UCnMo;70$&Vuk@o7@<0%ex4?QC;$2nL#>tM16 zM2dC0s&MW|#)G9QVV9VF3Y)?vcQxY` zg7NHwf(@(cG{qwX0PU@iq%f;kC#z-?ncUN&crjrzMn!;C;Z|04>QKch9geT5)lnHp&%96*sTuQ*)=rzNBDJBuYn|6MKk9x}6Rr)4 zC(Tt076*E*&MN`dq^J@b@`lYN(5PM7Z!Gm{?#0{*=fsxlsWVv{P<#E2_)^$+VK6c{ z-Zzmt;hS+)^3@Fwb@E}!BAmR)PG z6t76gKmzVfQpi;7Boqso~Qk2<_|zP=+sm|n#Q*%qppHztQQ$Gblxfyq4S zY(qsoUo*`(s;9M0Q;zD8=6&nr*2do?zMMDds78^VUh+dt6SDT=>sPxCeZ9A{P$JD4 zO?w3J5RLsAfx+aL&dK$Y^5i+4{p7llbx{IpiWW&yuku$dN`TRd{@L5Wo17H_(=s zJ`pcU#WopwZm^2PBY$jgH31* zPt&ejbp8~bKVC3P7tNb&Q8Cb&`1J3z5){5^f$snsIg&xnYKqX$jwDHe_SsvSqMMo` z{*xnrg_K>;_%gbG)Ri);EEw;*qfj5)7}ywOLKS1x6V6#h=_3-T3U4V3rj!MIseg#1 zQUsF^(67JSNMdqO=5U?B2Y+CAhJ@Ap=HOK^eE!W7HBxwr|H_#mNPSPx521zd0P#i- z+7ZdOUGAQ-*In$6FyWRl%|zeznzsi-^sU9s7;$Ss(yE6lpfZONM)fnwj0IyN zx*w@XzT7!u-*mC_iCM*U>)Ult%sPYBQ}#{h2N9<@N&}Mv^_@*57xZRy;_Mm=Fj%50 zV6mPqcx`o1?@ySsI?s4cdEUrH_s^D)z?OyF=aR_3%9?N)I(^=ZgHJ^7oL_~5ckrK{Z=sa;1dA~K;yMT(YEg60 zfnK~^9V7m|%Z^k`{G_JnuF3OAjHeIPKx+tYMHWBxfCBd9X?okKLX>dDL4*pC`$|bR z&eufV9ku?XbVM3T3&U~oM9ZwY9A`uOEItC&Cco;DXK7D#@=Adq=0CV{4?+#kUo9b_ zmH+db?~34U{@PD|31Kcj@YW0gxAS%HKmxY%^*^m6ExjVKVXX2qAdHJ7pfpiJm_`}? dt#=O-_*gFcMXj#c05?H@>t8@@;>BN3;6Lx*C2{}& diff --git a/Utils/__pycache__/LogManager.cpython-312.pyc b/Utils/__pycache__/LogManager.cpython-312.pyc index 2607057c369146f2ec04baf0459c453df7218785..7d1bd6e1d99c3365ff1897afbd8ac4c2004237f2 100644 GIT binary patch delta 19 ZcmX?Jbi9b`G%qg~0}!m3wUNu#5&%OT1>XPw delta 19 ZcmX?Jbi9b`G%qg~0}v=SZRE1G1OPvK1wa4*