From 01e18bdc03ad64f9664499477ef058fa4f306cd4 Mon Sep 17 00:00:00 2001 From: milk <53408947@qq.com> Date: Wed, 5 Nov 2025 17:07:51 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Module/DeviceInfo.py | 343 +++++++++++++----- Module/FlaskService.py | 143 +++++--- Module/FlaskSubprocessManager.py | 213 +++++++---- Module/__pycache__/DeviceInfo.cpython-312.pyc | Bin 31309 -> 40229 bytes .../__pycache__/FlaskService.cpython-312.pyc | Bin 40296 -> 42569 bytes .../FlaskSubprocessManager.cpython-312.pyc | Bin 13518 -> 16948 bytes Module/__pycache__/Main.cpython-312.pyc | Bin 3700 -> 3700 bytes Utils/__pycache__/LogManager.cpython-312.pyc | Bin 14663 -> 14663 bytes .../__pycache__/ThreadManager.cpython-312.pyc | Bin 15186 -> 15625 bytes .../__pycache__/ScriptManager.cpython-312.pyc | Bin 69108 -> 70439 bytes 10 files changed, 497 insertions(+), 202 deletions(-) diff --git a/Module/DeviceInfo.py b/Module/DeviceInfo.py index d24773d..349ea48 100644 --- a/Module/DeviceInfo.py +++ b/Module/DeviceInfo.py @@ -3,6 +3,8 @@ 极简稳定版设备监督器(DeviceInfo):加详细 print 日志 - 每个关键节点都会 print,便于人工观察执行到哪一步 - 保留核心逻辑:监听上下线 / 启动 WDA / 起 iproxy / 通知前端 + - 并发提速:_add_device 异步化(受控并发) + - iproxy 守护:本地端口 + /status 探活,不通则自愈重启;连续失败达阈值才移除 """ import os import time @@ -129,72 +131,214 @@ class DeviceInfo: self._last_seen: Dict[str, float] = {} self._manager = FlaskSubprocessManager.get_instance() self._iproxy_path = self._find_iproxy() + + # iproxy 连续失败计数(守护用) + self._iproxy_fail_count: Dict[str, int] = {} + LogManager.info("DeviceInfo 初始化完成", udid="system") print("[Init] DeviceInfo 初始化完成") - threading.Thread(target=self.check_iproxy_ports).start() + # iproxy 守护线程(端口+HTTP探活 → 自愈重启 → 达阈值才移除) + threading.Thread(target=self.check_iproxy_ports, daemon=True).start() - # =============== 核心:端口连通性检测(HTTP 方式) ================= - def _is_local_port_open(self, port: int, udid: str, timeout: float = 5) -> bool: - """ - 使用 HTTP 方式检测:向 http://127.0.0.1:port/ 发送一次 HEAD 请求。 - 只要建立连接并收到合法的 HTTP 响应(任意 1xx~5xx 状态码),即认为 HTTP 可达。 - 遇到连接失败、超时、协议不对等异常,视为不可用。 - """ - if not isinstance(port, int) or port <= 0 or port > 65535: - LogManager.error("端口不可用(非法端口号)", udid=udid) - return False + self._initialized = True # 标记已初始化 + # =============== 并发添加设备:最小改动(包装 _add_device) =============== + def _ensure_add_executor(self): + """ + 懒加载:首次调用 _add_device 时初始化线程池与去重集合。 + 不改 __init__,避免对现有初始化节奏有影响。 + """ + if not hasattr(self, "_add_lock"): + self._add_lock = threading.RLock() + if not hasattr(self, "_adding_udids"): + self._adding_udids = set() + if not hasattr(self, "_add_executor") or self._add_executor is None: + import os + from concurrent.futures import ThreadPoolExecutor + max_workers = max(2, min(6, (os.cpu_count() or 4) // 2)) + self._add_executor = ThreadPoolExecutor( + max_workers=max_workers, + thread_name_prefix="dev-add" + ) + try: + LogManager.info(f"[Init] Device add executor started, max_workers={max_workers}", udid="system") + except Exception: + pass + + def _safe_add_device(self, udid: str): + """ + 后台执行真正的新增实现(_add_device_impl): + - 任何异常只记日志,不抛出 + - 无论成功与否,都在 finally 里清理“正在添加”标记 + """ try: - # HEAD 更轻;若后端对 HEAD 不友好,可改为 "GET", "/" - conn = http.client.HTTPConnection("127.0.0.1", int(port), timeout=timeout) - conn.request("HEAD", "/") - resp = conn.getresponse() - status = resp.status - # 读到响应即可关闭 - conn.close() - # 任何合法 HTTP 状态码都说明“HTTP 服务在监听且可交互”,包括 404/401/403/5xx - if 100 <= status <= 599: - return True - else: - LogManager.error(f"HTTP状态码异常: {status}", udid=udid) - return False - + self._add_device_impl(udid) # ← 这是你原来的重逻辑(见下方) except Exception as e: - # 连接被拒绝、超时、不是HTTP协议正确响应(比如返回了非HTTP的字节流)都会到这里 - LogManager.error(f"HTTP检测失败:{e}", udid=udid) + try: + LogManager.method_error(f"_add_device_impl 异常:{e}", "_safe_add_device", udid=udid) + except Exception: + pass + finally: + with self._add_lock: + self._adding_udids.discard(udid) + + def _add_device(self, udid: str): + """ + 并发包装器:保持所有调用点不变(listen 里仍然调用 _add_device)。 + - 懒加载线程池 + - 同一 udid 防重提交 + - 真实重逻辑放到 _add_device_impl(下方,已把你的原始实现迁过去) + """ + self._ensure_add_executor() + with self._add_lock: + if udid in self._adding_udids: + return + self._adding_udids.add(udid) + try: + self._add_executor.submit(self._safe_add_device, udid) + except Exception as e: + with self._add_lock: + self._adding_udids.discard(udid) + try: + LogManager.method_error(f"提交新增任务失败:{e}", "_add_device", udid=udid) + except Exception: + pass + + # =============== iproxy 健康检查 / 自愈 =============== + def _iproxy_tcp_probe(self, port: int, timeout: float = 0.6) -> bool: + """快速 TCP 探测:能建立连接即认为本地监听正常。""" + try: + with socket.create_connection(("127.0.0.1", int(port)), timeout=timeout): + return True + except Exception: return False - # =============== 一轮检查:发现不通就移除 ================= + def _iproxy_http_status_ok_quick(self, port: int, timeout: float = 1.2) -> bool: + """ + 轻量 HTTP 探测:GET /status + - 成功返回 2xx/3xx 视为 OK + - 4xx/5xx 也说明链路畅通(服务可交互),这里统一认为 OK(避免误判) + """ + try: + conn = http.client.HTTPConnection("127.0.0.1", int(port), timeout=timeout) + conn.request("GET", "/status") + resp = conn.getresponse() + _ = resp.read(128) + code = getattr(resp, "status", 0) + conn.close() + # 任何能返回 HTTP 的,都说明“有服务可交互” + return 100 <= code <= 599 + except Exception: + return False + + def _iproxy_health_ok(self, udid: str, port: int) -> bool: + """综合健康判断:先 TCP,后 HTTP /status。两者任一失败即为不健康。""" + # 先看端口是否真在监听 + if not self._iproxy_tcp_probe(port, timeout=0.6): + return False + # 再看链路到后端是否通(WDA 会回应 /status) + if not self._iproxy_http_status_ok_quick(port, timeout=1.2): + return False + return True + + def _restart_iproxy(self, udid: str, port: int) -> bool: + """干净重启 iproxy:先杀旧的,再启动新的,并等待监听。""" + print(f"[iproxy-guard] 准备重启 iproxy {udid} on {port}") + proc = None + with self._lock: + old = self._iproxy.get(udid) + try: + if old: + self._kill(old) + except Exception as e: + print(f"[iproxy-guard] 杀旧进程异常 {udid}: {e}") + + # 重新拉起 + try: + proc = self._start_iproxy(udid, local_port=port) + except Exception as e: + print(f"[iproxy-guard] 重启失败 {udid}: {e}") + proc = None + + if not proc: + return False + + # 写回进程表 + with self._lock: + self._iproxy[udid] = proc + + print(f"[iproxy-guard] 重启成功 {udid} port={port}") + return True + + # =============== 一轮检查:先自愈,仍失败才考虑移除 ================= def check_iproxy_ports(self, connect_timeout: float = 3) -> None: + """ + 周期性巡检(默认每 10s 一次): + - 在线设备(type=1): + 1) 先做 TCP 探测(127.0.0.1:screenPort) + 2) 再做 HTTP /status 探测 + 3) 任一失败 → 尝试自愈重启 iproxy;若仍失败,累计失败计数 + 4) 连续失败次数 >= 3 才移除设备(避免短暂抖动) + """ + # 启动延迟,等新增流程跑起来,避免误判 time.sleep(20) + + FAIL_THRESHOLD = 3 # 连续失败阈值 + INTERVAL_SEC = 10 # 巡检间隔 + while True: snapshot = list(self._models.items()) # [(deviceId, DeviceModel), ...] for device_id, model in snapshot: try: - # 只处理在线且端口合法的设备 if model.type != 1: + # 离线设备清零计数 + self._iproxy_fail_count.pop(device_id, None) continue + port = int(model.screenPort) if port <= 0 or port > 65535: continue - ok = self._is_local_port_open(port, timeout=connect_timeout, udid=device_id) - if not ok: - print(f"[iproxy-check] 端口不可连,移除设备 deviceId={device_id} port={port}") + # 健康探测 + ok = self._iproxy_health_ok(device_id, port) + if ok: + # 健康则清零失败计数 + if self._iproxy_fail_count.get(device_id): + self._iproxy_fail_count[device_id] = 0 + # print(f"[iproxy-check] OK deviceId={device_id} port={port}") + continue + + # 第一次失败:尝试自愈重启 + print(f"[iproxy-check] 探活失败,准备自愈重启 deviceId={device_id} port={port}") + healed = self._restart_iproxy(device_id, port) + + # 重启后再探测一次 + ok2 = self._iproxy_health_ok(device_id, port) if healed else False + if ok2: + print(f"[iproxy-check] 自愈成功 deviceId={device_id} port={port}") + self._iproxy_fail_count[device_id] = 0 + continue + + # 自愈失败:累计失败计数 + fails = self._iproxy_fail_count.get(device_id, 0) + 1 + self._iproxy_fail_count[device_id] = fails + print(f"[iproxy-check] 自愈失败 ×{fails} deviceId={device_id} port={port}") + + # 达阈值才移除(避免误杀) + if fails >= FAIL_THRESHOLD: + print(f"[iproxy-check] 连续失败 {fails} 次,移除设备 deviceId={device_id} port={port}") try: - self._remove_device(device_id) # 这里面可安全地改 self._models + self._remove_device(device_id) except Exception as e: print(f"[iproxy-check] _remove_device 异常 deviceId={device_id}: {e}") - else: - # 心跳日志按需开启,避免刷屏 - # print(f"[iproxy-check] OK deviceId={device_id} port={port}") - pass + finally: + self._iproxy_fail_count.pop(device_id, None) except Exception as e: print(f"[iproxy-check] 单设备检查异常: {e}") - # 8秒间隔 - time.sleep(10) + + time.sleep(INTERVAL_SEC) def listen(self): LogManager.method_info("进入主循环", "listen", udid="system") @@ -221,7 +365,7 @@ class DeviceInfo: if (now - self._first_seen.get(udid, now)) >= self.ADD_STABLE_SEC: print(f"[Add] 检测到新设备: {udid}") try: - self._add_device(udid) + self._add_device(udid) # ← 并发包装器 except Exception as e: LogManager.method_error(f"新增失败:{e}", "listen", udid=udid) print(f"[Add] 新增失败 {udid}: {e}") @@ -265,65 +409,83 @@ class DeviceInfo: print(f"[WDA] /status@{local_port} 等待超时 {udid}") return False - - def _add_device(self, udid: str): + # ---------------- 原 _add_device 实现:整体改名为 _add_device_impl ---------------- + def _add_device_impl(self, udid: str): print(f"[Add] 开始新增设备 {udid}") if not self._trusted(udid): print(f"[Add] 未信任设备 {udid}, 跳过") return - try: - dev = tidevice.Device(udid) - major = int(dev.product_version.split(".")[0]) - except Exception: - major = 0 - - if not self._wda_http_status_ok_once(udid): - if major > 17: - print("进入iOS17设备的分支") - out = IOSActivator().activate(udid) - print("wda启动完成") - else: - print(f"[WDA] iOS<=17 启动 WDA app_start (port={wdaScreenPort})") - dev = tidevice.Device(udid) - dev.app_start(WdaAppBundleId) - time.sleep(2) - if not self._wait_wda_ready_http(udid, self.WDA_READY_TIMEOUT): - print(f"[WDA] WDA 未在超时内就绪, 放弃新增 {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 - - if not (w and h and s): - print(f"[Screen] 屏幕信息仍为空,继续添加 {udid}") - + # 先分配一个“正式使用”的本地端口,并立即起 iproxy(只起这一回) port = self._alloc_port() - print(f"[iproxy] 准备启动 iproxy 映射 {port}->{wdaScreenPort}") + print(f"[iproxy] 准备启动 iproxy 映射 {port}->{wdaScreenPort} (正式)") proc = self._start_iproxy(udid, local_port=port) if not proc: self._release_port(port) print(f"[iproxy] 启动失败,放弃新增 {udid}") return + # 判断 WDA 是否已就绪;如果未就绪,按原逻辑拉起 WDA 并等到就绪 + try: + dev = tidevice.Device(udid) + major = int(dev.product_version.split(".")[0]) + except Exception: + major = 0 + + # 直接用“正式端口”探测 /status,避免再启一次临时 iproxy + if not self._wait_wda_ready_on_port(udid, local_port=port, total_timeout_sec=3.0): + # 如果还没起来,按你原逻辑拉起 WDA 再等 + if major > 17: + print("进入iOS17设备的分支") + try: + IOSActivator().activate(udid) + print("wda启动完成") + except Exception as e: + print(f"[WDA] iOS17 激活异常: {e}") + else: + print(f"[WDA] iOS<=17 启动 WDA app_start (port={wdaScreenPort})") + try: + dev = tidevice.Device(udid) + dev.app_start(WdaAppBundleId) + time.sleep(2) + except Exception as e: + print(f"[WDA] app_start 异常: {e}") + + if not self._wait_wda_ready_on_port(udid, local_port=port, total_timeout_sec=self.WDA_READY_TIMEOUT): + print(f"[WDA] WDA 未在超时内就绪, 放弃新增 {udid}") + # 清理已起的正式 iproxy + try: + self._kill(proc) + except Exception: + pass + self._release_port(port) + return + + print(f"[WDA] WDA 就绪,准备获取屏幕信息 {udid}") + time.sleep(0.5) + + # 带超时的屏幕信息获取(保留你原有容错/重试) + 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 + if not (w and h and s): + print(f"[Screen] 屏幕信息仍为空,继续添加 {udid}") + + # 写入模型 & 发送前端 with self._lock: model = DeviceModel(deviceId=udid, screenPort=port, width=w, height=h, scale=s, type=1) model.ready = True self._models[udid] = model self._iproxy[udid] = proc self._port_by_udid[udid] = port + if hasattr(self, "_iproxy_fail_count"): + self._iproxy_fail_count[udid] = 0 print(f"[Manager] 准备发送设备数据到前端 {udid}") self._manager_send(model) @@ -343,6 +505,7 @@ class DeviceInfo: self._port_by_udid.pop(udid, None) self._first_seen.pop(udid, None) self._last_seen.pop(udid, None) + self._iproxy_fail_count.pop(udid, None) # --- 2. 锁外执行重操作 --- # 杀进程 @@ -528,11 +691,23 @@ class DeviceInfo: print(f"[Proc] 结束进程异常: {e}") def _manager_send(self, model: DeviceModel): + """ + 轻量自愈:首次 send 失败 → start() 一次并重试一次;不抛异常。 + 这样 34566 刚起时不丢“上车”事件,前端更快看到设备。 + """ try: - self._manager.send(model.toDict()) - print(f"[Manager] 已发送前端数据 {model.deviceId}") + if self._manager.send(model.toDict()): + print(f"[Manager] 已发送前端数据 {model.deviceId}") + return except Exception as e: - print(f"[Manager] 发送异常: {e}") + print(f"[Manager] 首次发送异常: {e}") + # 自愈:拉起一次并重试一次 + try: + if self._manager.start() and self._manager.send(model.toDict()): + print(f"[Manager] 重试发送成功 {model.deviceId}") + return + except Exception as e: + print(f"[Manager] 重试发送异常: {e}") def _find_iproxy(self) -> str: env_path = os.getenv("IPROXY_PATH") @@ -545,4 +720,4 @@ class DeviceInfo: if path.is_file(): print(f"[iproxy] 使用默认路径 {path}") return str(path) - raise FileNotFoundError(f"iproxy 不存在: {path}") + raise FileNotFoundError(f"iproxy 不存在: {path}") \ No newline at end of file diff --git a/Module/FlaskService.py b/Module/FlaskService.py index b0a1427..c677d00 100644 --- a/Module/FlaskService.py +++ b/Module/FlaskService.py @@ -103,39 +103,59 @@ def _apply_device_event(obj: Dict[str, Any]): # ============ 设备事件 socket 监听 ============ def _handle_conn(conn: socket.socket, addr): - """统一的连接处理函数(外部全局,避免内嵌函数覆盖)""" + """统一的连接处理函数(拆 JSON 行 → 正常化 type → 应用到 listData)""" try: with conn: + try: + conn.settimeout(3.0) # 避免永久阻塞 + except Exception: + pass + buffer = "" while True: - data = conn.recv(1024) - if not data: # 对端关闭 - break - buffer += data.decode('utf-8', errors='ignore') - # 按行切 JSON;发送端每条以 '\n' 结尾 - while True: - line, sep, buffer = buffer.partition('\n') - if not sep: + try: + data = conn.recv(1024) + if not data: # 对端关闭 break - line = line.strip() - if not line: - continue - try: - obj = json.loads(line) - except json.JSONDecodeError as e: - LogManager.warning(f"[SOCKET][WARN] 非法 JSON 丢弃: {line[:120]} err={e}") - continue - dev_id = obj.get("deviceId") - typ = _normalize_type(obj.get("type", 1)) - obj["type"] = typ - LogManager.info(f"[SOCKET][RECV] deviceId={dev_id} type={typ} keys={list(obj.keys())}") - _apply_device_event(obj) - LogManager.info(f"[SOCKET][APPLY] deviceId={dev_id} type={typ}") + buffer += data.decode('utf-8', errors='ignore') + + # 按行切 JSON;发送端每条以 '\n' 结尾 + while True: + line, sep, buffer = buffer.partition('\n') + if not sep: + break + line = line.strip() + if not line: + continue + try: + obj = json.loads(line) + except json.JSONDecodeError as e: + LogManager.warning(f"[SOCKET][WARN] 非法 JSON 丢弃: {line[:120]} err={e}") + continue + + dev_id = obj.get("deviceId") + typ = _normalize_type(obj.get("type", 1)) + obj["type"] = typ # 规范 1/0 + LogManager.info(f"[SOCKET][RECV] deviceId={dev_id} type={typ} keys={list(obj.keys())}") + + try: + _apply_device_event(obj) # ← 保持你的原设备增删逻辑 + LogManager.info(f"[SOCKET][APPLY] deviceId={dev_id} type={typ}") + except Exception as e: + # 单条业务异常不让线程死 + LogManager.error(f"[DEVICE][APPLY_EVT][ERROR] {e}") + + except (socket.timeout, ConnectionResetError, BrokenPipeError): + # 连接级异常:关闭该连接,回到 accept + break + except Exception as e: + LogManager.warning(f"[SOCKET][WARN] recv error: {e}") + break except Exception as e: LogManager.error(f"[SOCKET][ERROR] 连接处理异常: {e}") def start_socket_listener(): - """启动设备事件监听(与 HTTP 端口无关,走 FLASK_COMM_PORT)""" + """启动设备事件监听(仅走 FLASK_COMM_PORT;增强健壮性,不改业务)""" # 统一使用 FLASK_COMM_PORT,默认 34566 port = int(os.getenv('FLASK_COMM_PORT', 34566)) LogManager.info(f"Received port from environment: {port}") @@ -146,29 +166,64 @@ def start_socket_listener(): print("未获取到通信端口,跳过Socket监听") return - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - - try: - s.bind(('127.0.0.1', port)) - print(f"[INFO] Socket successfully bound to port {port}") - LogManager.info(f"[INFO] Socket successfully bound to port {port}") - except Exception as bind_error: - print(f"[ERROR]端口绑定失败: {bind_error}") - LogManager.info(f"[ERROR]端口绑定失败: {bind_error}") - return - - s.listen() - LogManager.info(f"[INFO] Socket listener started on port {port}, waiting for connections...") - print(f"[INFO] Socket listener started on port {port}, waiting for connections...") - + backoff = 0.5 # 自愈退避,起于 0.5s,上限 8s while True: + s = None try: - conn, addr = s.accept() + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + except Exception as e: + LogManager.warning(f"[SOCKET][WARN] setsockopt SO_REUSEADDR failed: {e}") + + try: + s.bind(('127.0.0.1', port)) + print(f"[INFO] Socket successfully bound to port {port}") + LogManager.info(f"[INFO] Socket successfully bound to port {port}") + except Exception as bind_error: + print(f"[ERROR]端口绑定失败: {bind_error}") + LogManager.info(f"[ERROR]端口绑定失败: {bind_error}") + # 绑定失败通常是端口未释放/竞争,退避后重试 + time.sleep(backoff) + backoff = min(backoff * 2, 8.0) + continue + + s.listen() + try: + s.settimeout(1.5) # accept 超时,便于检查自愈循环 + except Exception: + pass + + LogManager.info(f"[INFO] Socket listener started on port {port}, waiting for connections...") + print(f"[INFO] Socket listener started on port {port}, waiting for connections...") + # 监听成功 → 退避复位 + backoff = 0.5 + + while True: + try: + conn, addr = s.accept() + except socket.timeout: + # 定期“透气”,避免永久卡死;继续等待 + continue + except Exception as e: + # 发生 accept 级错误:重建 socket(进入外层 while 自愈) + LogManager.error(f"[ERROR] accept 失败: {e}") + break + + # 每个连接独立线程处理,保持你原来的做法 + threading.Thread(target=_handle_conn, args=(conn, addr), daemon=True).start() + except Exception as e: - LogManager.error(f"[ERROR] accept 失败: {e}") - continue - threading.Thread(target=_handle_conn, args=(conn, addr), daemon=True).start() + # 任意未兜住的异常,记录并进入退避自愈 + LogManager.error(f"[SOCKET][ERROR] 监听主循环异常: {e}") + time.sleep(backoff) + backoff = min(backoff * 2, 8.0) + finally: + try: + if s: + s.close() + except Exception: + pass # 独立线程启动 Socket 服务 + 看门狗 listener_thread = threading.Thread(target=start_socket_listener, daemon=True) diff --git a/Module/FlaskSubprocessManager.py b/Module/FlaskSubprocessManager.py index 94b83be..528b4b1 100644 --- a/Module/FlaskSubprocessManager.py +++ b/Module/FlaskSubprocessManager.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import subprocess import sys import threading @@ -14,8 +15,10 @@ from Utils.LogManager import LogManager class FlaskSubprocessManager: + """Flask 子进程守护 + 看门狗 + 稳定增强""" + _instance: Optional['FlaskSubprocessManager'] = None - _lock: threading.Lock = threading.Lock() + _lock = threading.Lock() def __new__(cls): with cls._lock: @@ -29,48 +32,75 @@ class FlaskSubprocessManager: self.comm_port = 34566 self._stop_event = threading.Event() self._monitor_thread: Optional[threading.Thread] = None - # 新增:启动前先把可能残留的 Flask 干掉 + + # 看门狗参数 + self._FAIL_THRESHOLD = int(os.getenv("FLASK_WD_FAIL_THRESHOLD", "3")) # 连续失败多少次重启 + self._COOLDOWN_SEC = float(os.getenv("FLASK_WD_COOLDOWN", "8.0")) # 两次重启间隔 + self._MAX_RESTARTS = int(os.getenv("FLASK_WD_MAX_RESTARTS", "5")) # 10分钟最多几次重启 + self._RESTART_WINDOW = 600 # 10分钟 + self._restart_times: List[float] = [] + self._fail_count = 0 + self._last_restart_time = 0.0 + + # Windows 隐藏子窗口启动参数 + self._si = None + if os.name == "nt": + si = subprocess.STARTUPINFO() + si.dwFlags |= subprocess.STARTF_USESHOWWINDOW + si.wShowWindow = 0 + self._si = si + self._kill_orphan_flask() atexit.register(self.stop) - LogManager.info("FlaskSubprocessManager 单例已初始化", udid="system") + self._log("info", "FlaskSubprocessManager 初始化完成") + # ========= 日志工具 ========= + def _log(self, level: str, msg: str, udid="system"): + """同时写 LogManager + 控制台""" + try: + if level == "info": + LogManager.info(msg, udid=udid) + elif level in ("warn", "warning"): + LogManager.warning(msg, udid=udid) + elif level == "error": + LogManager.error(msg, udid=udid) + else: + LogManager.info(msg, udid=udid) + except Exception: + pass + print(msg) + + # ========= 杀残留 Flask ========= def _kill_orphan_flask(self): - """根据端口 34566 把遗留进程全部杀掉""" try: if os.name == "nt": - # Windows - out = subprocess.check_output( - ["netstat", "-ano"], - text=True, startupinfo=self._si - ) + out = subprocess.check_output(["netstat", "-ano"], text=True, startupinfo=self._si) for line in out.splitlines(): if f"127.0.0.1:{self.comm_port}" in line and "LISTENING" in line: pid = int(line.strip().split()[-1]) if pid != os.getpid(): subprocess.run(["taskkill", "/F", "/PID", str(pid)], - startupinfo=self._si, - capture_output=True) + startupinfo=self._si, capture_output=True) + self._log("warn", f"[FlaskMgr] 杀死残留进程 PID={pid}") else: - # macOS / Linux - out = subprocess.check_output( - ["lsof", "-t", f"-iTCP:{self.comm_port}", "-sTCP:LISTEN"], - text=True - ) + out = subprocess.check_output(["lsof", "-t", f"-iTCP:{self.comm_port}", "-sTCP:LISTEN"], text=True) for pid in map(int, out.split()): if pid != os.getpid(): os.kill(pid, 9) + self._log("warn", f"[FlaskMgr] 杀死残留进程 PID={pid}") except Exception: pass - # ---------- 启动 ---------- + # ========= 启动 ========= def start(self): with self._lock: if self._is_alive(): - LogManager.warning("子进程已在运行,无需重复启动", udid="system") + self._log("warn", "[FlaskMgr] 子进程已在运行,无需重复启动") return env = os.environ.copy() env["FLASK_COMM_PORT"] = str(self.comm_port) + exe_path = Path(sys.executable).resolve() if exe_path.name.lower() in ("python.exe", "pythonw.exe"): exe_path = Path(sys.argv[0]).resolve() @@ -80,13 +110,20 @@ class FlaskSubprocessManager: cmd = [str(exe_path), "--role=flask"] cwd = str(exe_path.parent) else: - cmd = [sys.executable, "-u", "-m", "Module.Main", "--role=flask"] - cwd = str(Path(__file__).resolve().parent) + project_root = Path(__file__).resolve().parents[1] + candidates = [ + project_root / "Module" / "Main.py", + project_root / "Main.py", + ] + main_path = next((p for p in candidates if p.is_file()), None) + if main_path: + cmd = [sys.executable, "-u", str(main_path), "--role=flask"] + else: + cmd = [sys.executable, "-u", "-m", "Module.Main", "--role=flask"] + cwd = str(project_root) - LogManager.info(f"准备启动 Flask 子进程: {cmd} cwd={cwd}", udid="system") + self._log("info", f"[FlaskMgr] 启动命令: {cmd}, cwd={cwd}") - # 关键:不再自己 open 文件,直接走 LogManager - # 用 PIPE 捕获,再转存到 system 级日志 self.process = subprocess.Popen( cmd, stdin=subprocess.DEVNULL, @@ -98,112 +135,140 @@ class FlaskSubprocessManager: bufsize=1, env=env, cwd=cwd, - start_new_session=True + start_new_session=True, + startupinfo=self._si ) - # 守护线程:把子进程 stdout → LogManager.info/system threading.Thread(target=self._flush_stdout, daemon=True).start() - LogManager.info(f"Flask 子进程已启动,PID={self.process.pid},端口={self.comm_port}", udid="system") + self._log("info", f"[FlaskMgr] Flask 子进程已启动,PID={self.process.pid}") if not self._wait_port_open(timeout=10): - LogManager.error("等待端口监听超时,启动失败", udid="system") + self._log("error", "[FlaskMgr] 启动失败,端口未监听") self.stop() - raise RuntimeError("Flask 启动后 10 s 内未监听端口") + raise RuntimeError("Flask 启动后 10s 内未监听端口") - self._monitor_thread = threading.Thread(target=self._monitor, daemon=True) - self._monitor_thread.start() - LogManager.info("端口守护线程已启动", udid="system") + if not self._monitor_thread or not self._monitor_thread.is_alive(): + self._monitor_thread = threading.Thread(target=self._monitor, daemon=True) + self._monitor_thread.start() + self._log("info", "[FlaskWD] 守护线程已启动") - # ---------- 实时把子进程 stdout 刷到 system 日志 ---------- + # ========= stdout捕获 ========= def _flush_stdout(self): + if not self.process or not self.process.stdout: + return for line in iter(self.process.stdout.readline, ""): if line: - LogManager.info(line.rstrip(), udid="system") - # 同时输出到控制台 - print(line.rstrip()) # 打印到主进程的控制台 + self._log("info", line.rstrip()) self.process.stdout.close() - # ---------- 发送 ---------- + # ========= 发送 ========= def send(self, data: Union[str, Dict, List]) -> bool: if isinstance(data, (dict, list)): data = json.dumps(data, ensure_ascii=False) try: with socket.create_connection(("127.0.0.1", self.comm_port), timeout=3.0) as s: s.sendall((data + "\n").encode("utf-8")) - LogManager.info(f"数据已成功发送到 Flask 端口:{self.comm_port}", udid="system") - return True + self._log("info", f"[FlaskMgr] 数据已发送到端口 {self.comm_port}") + return True except Exception as e: - LogManager.error(f"发送失败:{e}", udid="system") + self._log("error", f"[FlaskMgr] 发送失败: {e}") return False - # ---------- 停止 ---------- + # ========= 停止 ========= def stop(self): with self._lock: if not self.process: return pid = self.process.pid - LogManager.info(f"正在停止 Flask 子进程 PID={pid}", udid="system") + self._log("info", f"[FlaskMgr] 正在停止子进程 PID={pid}") try: - # 1. 杀整棵树(Windows 也适用) parent = psutil.Process(pid) for child in parent.children(recursive=True): - child.kill() + try: + child.kill() + except Exception: + pass parent.kill() - gone, alive = psutil.wait_procs([parent] + parent.children(), timeout=3) - for p in alive: - p.kill() # 保险再补一刀 - self.process.wait() + parent.wait(timeout=3) except psutil.NoSuchProcess: pass except Exception as e: - LogManager.error(f"停止子进程异常:{e}", udid="system") + self._log("error", f"[FlaskMgr] 停止子进程异常: {e}") finally: self.process = None self._stop_event.set() - # ---------- 端口守护 ---------- + # ========= 看门狗 ========= def _monitor(self): - LogManager.info("守护线程开始运行,周期性检查端口存活", udid="system") - while not self._stop_event.wait(1.0): - if not self._port_alive(): - LogManager.error("检测到端口不通,准备重启 Flask", udid="system") + self._log("info", "[FlaskWD] 看门狗线程启动") + verbose = os.getenv("FLASK_WD_VERBOSE", "0") == "1" + last_ok = 0.0 + + while not self._stop_event.wait(2.0): + alive = self._port_alive() + if alive: + self._fail_count = 0 + if verbose and (time.time() - last_ok) >= 60: + self._log("info", f"[FlaskWD] OK {self.comm_port} alive") + last_ok = time.time() + continue + + self._fail_count += 1 + self._log("warn", f"[FlaskWD] 探测失败 {self._fail_count}/{self._FAIL_THRESHOLD}") + + if self._fail_count >= self._FAIL_THRESHOLD: + now = time.time() + if now - self._last_restart_time < self._COOLDOWN_SEC: + self._log("warn", "[FlaskWD] 冷却中,跳过重启") + continue + + # 限速:10分钟内超过MAX_RESTARTS则不再重启 + self._restart_times = [t for t in self._restart_times if now - t < self._RESTART_WINDOW] + if len(self._restart_times) >= self._MAX_RESTARTS: + self._log("error", f"[FlaskWD] 10分钟内重启次数过多({len(self._restart_times)}次),暂停看门狗") + break + + self._restart_times.append(now) + self._log("warn", "[FlaskWD] 端口不通,准备重启 Flask") + with self._lock: - if self.process and self.process.poll() is None: + try: self.stop() - try: - self.start() - from Module.DeviceInfo import DeviceInfo - # 重新发送设备相关数据到flask - info = DeviceInfo() - for model in info._models.keys(): - self.send(model) - except Exception as e: - LogManager.error(f"自动重启失败:{e}", udid="system") - time.sleep(2) - - # ---------- 辅助 ---------- - def _is_port_busy(self, port: int) -> bool: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.settimeout(0.2) - return s.connect_ex(("127.0.0.1", port)) == 0 + time.sleep(1) + self.start() + self._fail_count = 0 + self._last_restart_time = now + self._log("info", "[FlaskWD] Flask 已成功重启") + from Module.DeviceInfo import DeviceInfo + info = DeviceInfo() + with info._lock: + for m in info._models.values(): + try: + self.send(m.toDict()) + except Exception: + pass + except Exception as e: + self._log("error", f"[FlaskWD] 自动重启失败: {e}") + time.sleep(3) + # ========= 辅助 ========= def _port_alive(self) -> bool: try: - with socket.create_connection(("127.0.0.1", self.comm_port), timeout=0.5): + with socket.create_connection(("127.0.0.1", self.comm_port), timeout=0.6): return True except Exception: return False def _wait_port_open(self, timeout: float) -> bool: - t0 = time.time() - while time.time() - t0 < timeout: + start = time.time() + while time.time() - start < timeout: if self._port_alive(): return True time.sleep(0.2) return False def _is_alive(self) -> bool: - return self.process is not None and self.process.poll() is None and self._port_alive() + return self.process and self.process.poll() is None and self._port_alive() @classmethod def get_instance(cls) -> 'FlaskSubprocessManager': diff --git a/Module/__pycache__/DeviceInfo.cpython-312.pyc b/Module/__pycache__/DeviceInfo.cpython-312.pyc index 0aeb4c2d4dc7d49f8abd5e557c853f396f9d3b9e..e15744f4bf37a312a147408ac835494b136a3e72 100644 GIT binary patch delta 14688 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 delta 6927 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 Mx&3PfsXX-m0TJPS=>Px# diff --git a/Module/__pycache__/FlaskService.cpython-312.pyc b/Module/__pycache__/FlaskService.cpython-312.pyc index 8be2a43b3d90b5c148e2df79383f9f0e968e0497..c90f3b26149c482169f294cbb02a5222e326c3c5 100644 GIT binary patch delta 8683 zcmbtZ3tW`fmH)o^=9?EYFu?F0m|=M6@D>%L!6@4B0)}x(HVLiiDsymPxN$kTOYV}GRrw(73=j0!6pPy8LR_-L zj9KlH1BN=|1FfJ>M=uy-sWvV$S5PT4VzH*hIm4*>z8t->ua+d{{qIb z%uaa{;~eu0!z43A%rdK(eQa06c4h~+irLOWyOMqsu9ZNeu}w;IHtLqhTw+k8h-JXu zAeZzoGkKs@j9K0$-7N(r20yi(ouq8?V1_oSGn}TU$7hENT0|4(2DEf_{0$w+^lYUz z$2mRV4`*6{YUIF74(HJI&kxNDhCg#c4-qA-FfMGeMN0`mHyM?UHXroe?@=i?1|Byp3$z?^v<0v7J)VT;{%@@I62yR zPTy>GIEze9(`V;jX!1`?wvY~K-!hOAc-$4Yrnb7Uvbb*Dnw14LRqOP`(y&c$A;eDR z>1juNT3Do4;jlQJ)-4u$tCJ){A9^N!5nD<>i%%%7hDTBX;FUP+4Vx`auXIe(t0}bG zY?cP6)o!b?fQDk!=?z&#?3*pNrPdaUz%VMvQmRXs9#n;zDiIdbua`vAO$ov6R(Qn! zjzVzW(~M7?!xyu|s?g7wOnIzN#Hk{N)tZAL`$LX#gX(0TjERiyEEx`q^odeJV~68( zN9P`zd#vv8f_+6d<8;H(@xz+fZu8O2hc=&_)-%0(^NobU0Zrk)5}$;D=8W8-jQoL& z{GPfS8H)y@josq*?uKJ|H+6<#ZQ`&l>FBOQyAHd$#W!_H!`ft@NF0+gVn}^6?ReVB zx*LYv?n0k}F{Hd1bv){1?2W`(-I9@n%##i8G@WWX*Vtpak+twfLP0k-oNDamx=RiP zjYJ!ch8zmr>z&@8>RDFjS-RY_e9hq64IYzu$YdR8H(76(Hs4s=>{+wLGu<{A zZTE@!n5FE9Hv1emsGT*UO+8wDsM?cN+Arzd(Z9{JVwGq4YEQ$<|f(K4tEVlN=CzMX(`yR((`sT}kzRX9L8u-icJa8@>c;rmOVJ@ffr8A|M zvpF#3@=O8qDefFNw)hBnSA4Ltj^WTnEz zJv<@E5t24;H`nzi|M$ekJ4M@$HnB4(pv|m;)D8{`hwq2=IdycQR!ev1DsmyOCIvdU zSaa|$o?*-(fxOwq3PX!`i_slK4W3QGnY6Q*I{T-L9!RN!fh-8tLf1!H8<0;6$YTl) z7_%uOKuvs)r)?eE75jJGn1u^o8U83Y+GJDRXsekrm6C^+MbH->F;(A$8;kx|ZiE&5 z=Fks2Hj8C!bEeoLWO-Z0gp-6Wp@Fr4CEN~wf2$Mn{Yl=B`$S#8f>gbnKAo(~`^mRI z{oy%%$>M_A%KF0UB}?j;R@c;hcK-0__r5!N{>P&o-yc2k_MNBS{Oo+!?Vde%4*ckL z&+DTv9;a859Sg^j^{oYUBo0<*SN4=d4Jj2n=NKY z-UTTuqV8hVF{Vi$Q2EsYdN8Gwev~pRt92T*k1H;&sj046r~l;e{?UDBZuk6T^rvrq z^71>Q=bwSR&eOk zrNfHgVWqm0^@;N25hLozgR1?iBaKIGhiqpWhq4w9K(1C7bQWNRv6>xLhaOb!R~}h0 z6rVK^pY;RDP)^}MPGRq|L3OE5rclM8fMma8)03u9Eld&^!Z>#7*5WQ1Y5YX{YdkRDZ0d#hil_9PVd zntNAxDw{moje|<-h%(fpdAz5nC(l!`&J(tNP+5N~GN!Zi7S=p6sCg0%Z$=-FJ`uaW zVqZ~r@;wa`8at%c52*ELls!eg$vy4)o~3nz>g7Y~H3RB3gX(p@7^n`ux;J~QCRoja zu|sOZfZE_mfz-dkQ@7HS^yH9w)qr}{1FRNSL-r09q$R2tVWuZFnO*!b?IlPpwVGjGIZ@00#o9qn7hid7Le!!^U2 zh=Zm3OAnOa9|_k!RG|FDx15F19^8xkX6X{iL?;~Fy?-~B4e=jqv_b{Jg~r~-3L<$x zlkA!HM9D`5Z4K!N3x zj)}DU=lE*5k7G0uU)>&?Xh87m&+RM|6yivPW8{V0+zJEpvy|)-E%RZ-+)@Q|F<1?p zi@MPAVCG^@I?$K2;pGX;rR+?g`?T^>Dc6@0T`Gdwf(OoJ5f7ZpQVwZ_ygZ1z+#X(_ zD3@^k@nYch8^X%+nEpI@ImcZQv4|2Lq^<;Ek1N6Q@;L5Flt9O_NXH3uLU=_2cV&h^ z&ypdXA6*f{UDe>2S7UhKTus2%tBLI5DCVj`UO9uinx0>o!ChN9jR)4X3?5k5W^hQ) z4qGf^uFK?$HQe=Z7BPxloX=g430s^gzMd{ZI#a%QuJn4IKtIMJJ(mN1ol!)#z{^eS z?mtgoDP|jKb!JO=8FI7;c=jbb0O*a(UE#P)}K2wCa^Nn_6uR8w*-m$oJt{i`JRJI)mFb zyO=E%dQJ=>9~%NBNdm%AgkuOA0IztXeTzkK74c7loWQnjdOt5watg>))}1&fMZ|W~ z(z)|;CKN0GzG9MsYL3%)=jsv##gZ|_$UZ5wA)G-tOFy5hQ=9`b8z*xDCX+~acdFf$ z^PD(vZ+=4iX%xf^H_3fL|GA8WS8x53Y|1f`6O%JLoqkP4-kHc@Gl7CW;*0_ z-FzM21Iz{Pj`^!Z?Shy6#-k)!cNZ$XK>qbD_&=E`bDCP}H`<%clPsO=xA6g`W43|g zG}O5;RB;~0UWU$lS%`#p7v_pG(MGSd!Ml-v z?(2nW(QLtCy~zAKAryw~I6S$8&`v{3;tMY$8G?XjoZ=^$(U06K2v-3dxB!z(_&GKp z*xXx7C<}qam(7bpqY3T<+sj{Osj(s|BxvHgtc`cZ(1pulr0d||BXP#j>dJUpRHJRUgnT9YPxQ5dX!?fqLwi;ZSXyDwlId$ zql+V>UI9&Bg$XtsJ6Q~s9r-CR$OrWL;(5!wDx1Y(t|KO!quJ!NkX~#WU*5ZS!7C7J zg0In$#%O|PDe@}>|MrQLuq$Bzk&Co!NqX$Bk;IEm@{MrVvm|ld!{OksOYv697$l73 zl~~PaCzhUqy&s?^FTx0d5Qh)P4Z-^;2b!j*e3d@zHc-#vN)rIQiqg|LRj1XrqP0h5 zhh)ce=TdG&Kh~+DA6G3@syr*#deYb5>DyzG)}uFuE< z-IvXm%%bk8IsNTEFyy(ySPd0WlpYk79$Ue%V~A zuav+|a_6#mHkR%$kAv8GeVLAvY)ZhDj^ZJL#4c&0cv1w(oQZH#s-;&T#&A;>KK_{T z({Q*S*rKDAHNmW$uCCFg$S1|iq#hVA0h^%D?`%4%TpKM8ts?(UZ`LHWW2lfJgkgl6 z2ic34L<%Bz!>@H;7CLveA&oH2)7Wv86~puU8Aiwsr->)qVNag z$Ef7@2>&29gd55SwGm;Tfbg3b7IkA`kxUJBuCS?L;ZxFMbxVWR^%r~6YcMYAgt!nD z^UNh)ULxZz1!tFV+@<*}(0v?VBBLG4yVy<|yuu(6X0?~*tw`bj0K<9Q8&>=`TO#a2 zAHwr}wHa=Av;C2swF&APugJccbVAd3MFRB1ZaK+oo%DN88h9Tx54o>Axt4x zevRg?Z2=2TtbI%J1v2i@+I26aPi&H5Q(xDIiwH>b()GouxO@8FN8vRWUdl0V3uB$c z_T!YSPh+d-8|#fc3y!?wzPA2HoJ0`lr7u2}IeR=fjKPl%i7?bPIaB=csc7|-HU9-I zuxjY^2DJn(Mi`CA{Y1k`kp!JZYUp80oa!qejnc8X(GM-jS|I`O{s@skCn80FdoPB3 z2HL`DWMhaz0guVBKRcd*OrUWWHZBlFqOW!ZO#x7LB*Y$x(}!ZG_^qz?v+HhH++y+?%ZhlF&b)etE02G4ruZhqZ~Q=)c<2;_>Zi zRRY|3{h2!lkKK9o+?_pdlm7%p6&a%9md8Y-nLg2?71c8Ir}kL7tz{wm6@9-Y3o`wk z7J~$hBI{`I))+mm)}*+}#m6K7uh?vDaFS43x>a8&LL8KG z;LP?0VK)`tEU$DsF||P1<(0O!K;T-;gkkAhTW9l`AiT(Zd+S-wUrmp%!FOBJBK=9& zD{6L-R5XCInjIvE{( z*d;Z!STi?UcHUQ+tnkZ4qqRwRmnOEQiJ>lGGaB-@o5ICrdZCG({q^``qd*ZZvM9ykHgXgQjHkCyG;084mkw-%PL zZ+EItvOrBFtc)Ncyh-;c*FJ||30=;*7qo|oN`Wmzz!iS{@SV4{z z*+}90L27}T@JDyYn3>Dz#g6IxYH0j`e%|o`yN|y6Oq0yTHo?8-X^y_0n-auUjc-b{ z{n?195L72PlOw|RE9`66==;wmDAs|Z0rW~f{pi{0?y%=#wW%ZKmc{%iVq)Sjb<2LUP;^{~k$w-_ZxQl@r(|tdFo2Dp9YAW^_(d zn1TEQPQM1zjEwpy1VNoQ8Pn>_MFwCGB4M#E!WH2TDP?9^q=64qr$i8;GPds z$+v(uhWL+nR#4{^*;*zICWvmPKY2Nk{hI#fO3GqyBwC^~t!o>J3IO%f=s2Ji+6H-&Z9&2^Bxb|8BvvK96g zi*1ZMKDbd#gk5UFZO7q^KT36qm%yG`9Qf>8;Ijytbl?THgPuK*5+TU9pxO-xLYCM; zKRM9B9;Mb-yVQ4)D-@U-q;66EwfwndKzikvUYE66S}o*7Y4XQt!X=Bu^ukC-Bp%g}j1dc)381AbcO;6at=t zNe{w#gg%6yBjEXiU};LQ5Fvj+z-&$+i?$0tzcU0g6T!4WaEB$hT@u`H2yQ0?w*!K2 zEWx);h-iX=M=%r!E-yi!dO6Z+I|bV(Do+ErFU#LzH@7xho**Zo9sV(n8*tWwD?*n2 zJrne6#{K$1F&mr97PI@d9ckz2f73(`&A^x39T#?^-AF&}nDBZ;qco z>#V)@TI=k!_S$RbUq2SzdRG+oky@=_;IWp-s%|=858D^b?x07a-(Z*c=Eh7CMJYIj zX=CJ!m&s;W#?Cf~-eOhK8jk#D#ff?TlUHl>>%*;zqR4+U*9KA0Msq zB#dHSYact=9x*cKgm_3d%QSz|{Jos9%kbZjWtORzr7#{DO;Q@@U*n~$hIX0_6?Umt z3}SLU17;OoX*)Ao(OOW_Al)f#VVfgD)OL0|^EkJb+0Ldg6BswUUBohLVU1D!5Z_}Q z^3%+a2dh%xbGk4B6(M;pZ^gClZ8$q*8`D`J5++>Vg z0-g{v!h8UWf-I%~W*VzpjGP4s3u#zlT4OnoRq)^a3y9+`CMY)WR<>Uq@!w3Gd==ZT zi#br(9~s>r6W>0yW9E|?$1^%AJ2xH6=+WnOhv&6OzKGWM zj~&-Hc3St?X}k zi>v(R>R#>U0qvO6T(9<#0d3>`sd*P9U5{VbcD4NKD!+MUueKD{sE{>)|m<*WRw*ZGUr`IndYm#z1& z-{>#5`!_oL85O;;m7m5Y{t)D0%J<)-2nuC_#vkrYrt6Y3>}3#$Cq&j5X76mhRmoj6 z=?YY$i<9%i;N=o0gMmw`D65RWWHMSg{!*?4_?I~zIG1G{(n{W{m0k|#K=QIyz!-U< zh`XE^Yt7{@=kh?)JFaNaA355b5)gUR!k%a`TY1*Y!k*n}PN3hJCu*c;Vf*95Vku#1 z#8$1yE~Cp*<5)SZP93k4dtuj{7?sd4J%M)T=;@e=YT7(2D#xxw5xeRHHEgwu%a*PvqE+c`p2WjPQA5UR+LSSuEu#L6 z$>|mynGD>3$mI@5Djgn&a~r``CrJpYI3#hq$~QYawD^&kH39L)DyLnj69Gw0l^f&( z5?gt>qt+wTjey*`C&1qq2R(4f9!%@*}|ZT;U)X$?Vh)H!GJhyf8)~KOa0Ng zUG}ck{yC+7ZCS6_HXxS!Rg*dkI;Z%juktHa_lk?ZP=rC5lI!~8^e1DF$F}R6791#O zG2fFh^2k0}Y_}}-l&Z6!%iKBhs<>CSxKFmSTeh-Swh9V}QuFP-C`P5b%W%9#@G#@_ zg3~koVbgkL(+3)dT+HsQ?p*JW&F_`X8IXnj5R`)S5BFl3@Hi+ja@~L`x=&^3RvG-J zDVvN$lJd?t>H_=pu_qN-AI?+2lJsp;*k8(VN+|+PTOvAmFJR5PPb&IB4~h9Z`kI z(;!dcaV>DWoMb-zW{O!^hAhtM+D;bJw5dk^0KC89n?Lmvkz_fLq=6>nn9}zNB!C+` zhD4GM7?9W6-0mXRW`~nB!}}j`ID@{PA4ThPZm1jJ)gmF#nK0xat(lRiK_wP0z}MP{ zZ3{U>+h?Rno(A5hjPHXP``M&JKnG+S>zwu)$9y{pna4SbAuX=lsZoL!G336>UQcie zIYQsbO_a0)Imq}v&)v+X3G9*i^852q7!P@J6yX_Ko2ONWoFd1ObBMO)B`HnkAqnU~7H0S5KHr1@)c(SEj2zfRZ?=!xLO8IF_z9T1nh?2dqR zi^J`?$-ePRYcP?YU6vEmg~MeC?*Lea_6PlPSw@WT zjp9+31mTNx{Bon>Ly(?>8aJXwTED!4o}VC>&f|o83+4XOp4W9qCDa#8DDp1VuFlYRBN>J8HxXXDI%(;H5q_ZOFM~+|NtGQP!*gA5@J-a@ zM-Z~=!7P9h9+ntOZ>|oHyapnRFjA!ecr`FmdGT3#ZNp^xZLvzu`!kl_4j(f+__1uT@Lvt`$mJ zxgwUXUwejH)=iHu!F#7C21JEtINrP`jt2DxxPi*)j&&*;Jt<1;NWGGwo)DVB4PwTVI;7pMJg0)#KVOnemzPAbGU_NX#mh!DtfM5M zw+y2vOEUDbowAjTooRr-BQ2ptvxIz1ua~4WVnUGLB7B1II|N+qA|k}`!IU7s$Kf6T zOY~3YLJ|te5qc5+fbdgEA%cB>Xnjh-PiBQYlz)Awq#xn$q{1C*42|5NiyQzsC#FLF z{ZvSz(>HiUK?VI_!!r3Qf5sfl1S@1hjiF$C!9?cLM7cGayEJ3GHH*8P&H{Zoi??Re zmmh0p8|c*1WQpMaK3ZCu%6|^pkNBEO|H4XyLh%kgXw#=XaQLI2!$oY=QZ&$yY{~o} z47dC4+8*PSLQ3bM_(fXcs0CvmJ6@LDM8<9Et!T<9LsBSNkq>XV(2A*j{-(gNbO&ql~^@~FXk()%Io`-^ciRRXrCEvkFKkKWkDHe@G zL&DKfhbnKhy*T)O+x4!euAlE1JoWmmU%p7b1hz$be?{n@on`}mxP=dW=?^&lF9`pO zaGgHlN>=uOZr9qIQAgb>`2Z-X9*M}=u`0$RO)d&?s+H|M%*S7Q91>s z#rCRl50TK?9f@q(qH|-H1NhZQLgz*Sj2on${B1}S1k$fGttlwjtFIWx>WywFQ zAC2hg?mcOI4v<#gxjkoDNS8JH(j>y}YoL4gnfY88+w6N~-`lMEHhd$Tqj@;-Hr=|v z%!ujxwF=<+`>)BbVsZ1vw zoZ&lv;5~M(U}2VE2B*zN3Y(WA3r7m@1@2o6nTvp_Mdl%ezL5Dqjo82V)csFcNWXk) ztYR4q?m=^h?wfmweV1Oy$)u)z3R+yNrK!yZdSWOHvOMPTA6x;Ym_b}`r0+CFtcw>qJR4m8Ip#IpShb}u-vb?;)|y=E61YU+h7 z`4foOVYR$@{@uaO*2Fq{m7Tl|@55ap(4j78q^3Vu>H_NWO*UtxV|IDDtIp|xI&>W8 z2q6<1mJ$DX7y$ochWggk;59)wfv)d>6;AT$4>z+_I61&?v(?l&ZVMfkaO@5>uEQ=p zcnSo<;YXAu4o|U*Y+f1Kw!=O@;FWL=uxNV?)wGULlmPh}dQb`;Oz>@Oy~DB3Q`^yl zA{|Tr>*y;6v=iXUw@nQ2&blqN_2e0Jp$*{_efydB#|HTF8kgH)(S+`rWuP!1a@LMM zc2IaDeYP!$y-VL~o60wUOqcKbwiTkJY3Pb@zKDqe<>bg1_8l(eTYhekGy?N0k?-)& zyY2@N8|_hdjK8_(Jh(a`cf(;0+esn-@}#tOVTwg{#)#yw4prZV?lqvWSU#QM%x{=J3yu zA@seEMD5JLJb&fg**@Z&85%wbNL3jco5g@?19UfxB*C%Po$>hc* zNR}hiBh&!|G&RC+WDZgq`dy4{LoQETbsmRs6%c;LAW!3jc7&4%crz!jBD{fs7rfB* z3NJ(C8Ul7$#oo%$8Wh_Zqr rpo00depPg}Mm%kQWBz(#&TFql^$#x`IuhG0X2n?!3=vXPJ_ca_Y;pPVrH zGKnE3nCXY@gp5r>FHVOf?#E1=p7e$!ogKQyh!lJP#(Me)A8OV0a>E(Px^yOg}I>QaF(bE^9^T^bVWpqxp4$z92P+AeKhN>@r> zYF8>BPdas7I!Kp0^?ime14+n;Z3L}&mY|hy$b=qsrJW$ns$2NdZYDYTo^H=!vz$}! z?)TW;u5Kr%*yn-(CvUb}J)FGF&UzqQQk%PXSGTLX*Tz_ddW5GU@^a@U1fGY1+6Ww6 zmuw#l7=|V5lG|V~Ign~o($q%qWrR)T&_~L}cyC0)U7BH{hot4ts=Jb~>s`roCatt- zX_ZaeBd68RD!Ni=4aBF?N$}Ls$?(+ETARV9wWZmNJruS^r`XaVcj~k9t_)fSIWlQI zJhS+C8Et_0Y&y-B6Uk?U_uNQYI)w9V`E(YY@vL0xRlzWEoa`#>Ak5iZ=2mAnd!%FF zP(S0g+E_L+GXpE2VLr(;|K{kI@4x)T^b7NE4c|O{(Ntyn;`|G@#-_h`VGP65znXvR zrTJI?asIvEv~wDZ-NkykT~?cwjF}H5gemyl`4S#KA;$*XZLkx?UA0snI*3wYCBc%%WF&C_ zdb)JJjA!X!hRoPr(bw=tZ`VsY7ySu9)%%$D6_=@T}iS;kRT zCku0EvBH|L77L3rVLEw#b<6sLRyS=sxaWlDu-kRezPqE*esGtY9&p+YCSJeV{u2yt z%)!DX(j$Gyf;{X&czk`52&d@J9yoR2D}vJ0hIMIYhfWRobY*jWe)f3C}7q)=@8pqCS*f6MYjy;%f6}dz}~?^=U1g zBCZ5UoFL4p?RMq!aI~|wwl#L_v^>^q+1l9JX6f9%x20qI?zU!*T*Vorw5Hth z>4o``pU%HEa&vgpT)?R!WS!Gk-FvLN9u{?M{o;Jpk>cS%3z57Uke53v1`(uI@e?m&8gOi%aGw zZr`5P_N}`)6@3gQrQ1ds2aH+7>9<#V5@bl!ytBTv-+<-5w}t#f03s0@KB`OF0*Ed{tYnGzE&Kgm6&_ zUou=(gMMB(FCYEvaLF?Cix!kbdNy=MJ(rwvw&7I6D1CbKoFOk{sPG#q0tWN&masPU z?2gkr&R2$V*ZJXJyY9@6a6#elmXY>wR?cwq$o9|F+K{@)uPzFzi|5p-XLYA^|0)|;RV1m^7(UF^D|2+TPgefZ6f?#&@#Y^+U z|2%(ohADtdC>{^cb{co^v2Mo2sqo2@02{-&nRc_1!KV1kOcREh;lXKIj$3WKIL#^g z86X5^IkN#%H{oM5J{sY{;&ht83CCeQybb0pbsXRzVFva%JWdh|@+6HRtV~jMMdh z*01Fc`+LZ5S+AT@>?TKxhA{(UwL8h@s-rf~!D!3ND174@N# zgy$m31o^$N0pW=o&;t>IOSJ7Gn=|xaDFtPVUKuS1{v$1|SSvwXR=j~Nf?3oDb6$#07hCw7W@j7D4iF%dSfNOOP7o3S%4lcu zA(>OTY#!j`o}8+P>}9 ze}LoE9^k#mj0a_PTL^#GG+(Xooemw_Y5#Li+jM+Kj2}uK;%FPJbu8_mtDQg z6Q-N5oV@wwzubK5g)e?GA>36>P{YHV+{wCo24yv#L1m4-vuV%zL2V6-zR680dp~%X$O}pR zcABG@0T)L>5`)AJ*jqOU5jaE!R+)&9QI3L9K{ugBj)GEgbQC4jLGw~{^jpFU&KLV6 zFIz0g3>w3n&T#hFsbk}1Kv^?Qq0Ab8X3g}8*>YcP<5jt@>&Z~pA%EAQV5W7H3g;J1 zwokMN@~dwXN<$Yp+8oX)ooc?+exco0y2_VTKP$hMwed=xZ%^m7#?HCI;;FJrl@}^s zs}7aa`%CHrC2M9Ff64kl;f8T4oL6|U#7Io3SAV^%)f^E>O+(vJ@N zs&U=zn#6;PSQX^F6%Z_jr%5w%6xkf|=|nHJ4X-o}FsC)Ow-ywVi>y6}Jh> zu%8@l3TNe>KQw8du)lmHkY%3B&Ye_EsHUi?N6+iR+4(4+OqKhp+pn@$ZNAFR>)HFh zQenZb@50SCo_FBI;+e+07T_I)a(tFHlHh%~tWgQxN7aoc@IFqd!RH3eRu%Q%l_WeD zF&Dg&@yz8_AUodyh(uTdl#rZARGEArPSOQeN(RJ=jv;j#1lr&)N(g}PN#rgOBYF_) z0U{?|HlcOt+8AwJB-to;Ax`FzSd9nyF7F(PSVcsxb4XV{F?W>l4FOwoWJlx0kRt)v z7iWor_n|_ z9bg{PtDw`M-}T?lb2;OUh!_YUiF6h`5{ThIHWcTeCOn>Tj!wIrDURBKB7U3`_a`q- zB%(b!Ityw}0IxV6iK_};7Yk)juIbx(=m6e}4MQ_ea0HF!F!igB$78t@9^uJ%48Y;+gr; zx93k!b2e(RlBKMr&tApFK%FbGP>`oiVwlcu($o^RT=e}lRugzsU-p_0rtXo1{<3-`x zji3$acCwplr2Z!lSygIb&d{?a~xOe=O}_*tJn_uJ3^<>GqNpt9OzJw`v=d0#Q$>8V-EsTfzf#v~m0ws?Vu$=19Ko4sl1bG#qKng153<8RW zN(~lNNT71#MR%AgCBShxJ!kDl;(X1nS+=`XF1jl>M6vGEm$*y zyoA&41$Y+LqXl6jrWB*p;$%PvBiON7Bfvy1J{?AF=?CR3r@>RixCfz|l(mm$+FVSW0T=A34{+*fiSsP78vGw-P|#{!@4ZW3Y47^^&2 zIaYVBE|69_+%&gr`E>f*g|8O|maQFb^688H+LF(+^3FH$x2LSCP}VAc)~Z0(>f!BS zT^4G@Lb?*at|Xu<^DS@j>ssdWiY`)@)ECs#h6~96_J;E6{dx6)yfvfhaJFeuH=zq< z*Z8w*0@-ylYy8>ku4V6@J^a4=U3Xy9?%~#d(`CH?XS=cvz%N-u7I`KeIRYJbt{Kv6@eXtTd)^A*xxv}IKLS-NRztv|hL+U`$p2*b-d ze|q(Tf+|cKO@doa0TP?(u3+)DtLcIK_EF6ZV?JD_CO1uNny&w~apu6U3a=FUa`&8T z`do~iNuL?I;_~J08*RdVR0lFEMyb!V`m?R4T1R_)rggKWvl+8JS30h=_zHIfwL3r0 zTNcWz^5<275|O`=F860HpUWy9d-~kd;Dxf5`?HpZGjhT?rf`-ioShR+&%*zb@-W;F zw@+-po0F2C3Mg~i-9jQIGo;P;YxAd?LuG6HWou?VL2YwLyTh-ArrN^MZtS>P|35eW zeq(6o5BxiS;M;M~SNKFw`{ZZFtdOz7Z>*S+`HU4o<0^40RQO8UgZkZ(*B>X(WtR9# zcLp-seEK$_&^2Slv?o;86%GE1hFQ=1L+=jx_8kaTbdAb` z`ZA~}q%ZU9%R>4}zrHf4uNGT^P1h9EH{WAP+;aE$EhlWy`z+p=&Xny^P`_!D!ShcF z#jeyP6sK!|sV#MbWHLc^fU4qMA^?*mW8tvxUoMd{mL^3h17NLX0`mp%55Y=2$#IUB zJs9j1>jK7HAqRH;eMjVw?y1oRMRw1t@2GZ8fV|Brfi~J9Bk0*s?*kg3ZW@sp0l-l* z5tzs7bh97^2=wg-kUv%uU`|2A(wd-HT6-Rg_Q3-*K|qhZr@d44mo{G57%E+Jt#nOT zn>Lr8J)S<9Kan5GsrKhohjLc?b5_qVf%Nsa37KZ)a5JYfN?Qr=n5qj^uhm@FuUJq( zDyVWaDQC5(KpB(gSLcP)#eQ}1b#*E8Fto_YfxdQgO2!7rn#&3VQgVn2AiuivbADGv z!9_3Wk@7{2F?uAFfywfy#99&|3aGt_YGsBev?=dHm;w9-3v!JyBNL7Np z0a7i3TR!1P)KW67jfboc0ERO}Jw=@WyCn3FkrEV}(!Ld>RISa$B39bXTJ84x8VPaj z6-J)xX8_d%iX65gvzMJZ++~#|< zGicbiK#(cxdc7NEIbx#FZ_^Q5%V2v_Oj>x@weZ}i%SI(CDiZ%tco9B$#-{9fSxzD0B_jc)) zO5%gGvMq(w2Wu)I{9&PDONHvgQWb_PHBD*MhpRH0QmBto6cAo?OTwedAM;l8cWL5DJN9IPgXg=e(qNhjz`qYjJQN{5UV&&1hh&eb}y=>Xb5#|iPl90;1hsivHCNf z7|EZKkNgmb5%^JJRQms6%+Zl7wz-o42~E2|zCgHGM84j94RDD0zdL{P%~vIGg4YIh z0)5mlaQ$LfxVPQH-Op1)fvlA*t|qR6`S+fgpPdEzmcrlv1X%$uNfFtgpB?bnot$cq zU_{BOt%vPS8t!b&0nCJi3-#`x9!hCHoG6^80y)*A%5XvHq-DY~;~BRE3O3#* zklrC7CF6!Zea@JV!Cj;% z5moq>ZM#ZdVL^5e>D&DJwr`*;q<;egUaHmm(Pr9)~ePxqb_Q3{y zQ?C5OSyu7p}S@Cf)1+{&gs%XhtEapp;`Z9=SJf)Us33M>x7OQ(axVL$i zi1yMHErHs!0+kU%ASwavOlVb^P+XV9r%@R@3FRa~PHZ<$iSamZp@^a-@4O__9<@YB zB;<*-&kGnK2wj>Za4eX5Xesg7!KyKl9VdYJ$*)M3oL~_s(fbFrIV2l-&I<~fL{=5F zb`barSSUuvlDJQ3Z?J@22Cah>New2>2@ixz>%s%r354RrC2@8}N#?!pIX<};seA(U z8_qSPB;Zd+fUh&@kd|ovQ)s;?j+4-jk!6rCfr74?IIFPAdSUi>G*$ta>WB|3CX0Z$xLopKVi|pu3@Y0^-uWdM zcF&)l6xs%nHmTWm)NZx4qG>o%y6B+4eE#R4cM~!SLtk%V-i9pZ{HWG~!FA`wQ07+< z0wZi5?-!=C24m!i_6v%P3e1Z~kwF^4RO16j#ZgWWbt^HX?Cu9u0WFBhD5rvY$!W9o zGp8|Zh-j^%eP(`(@hZ?8&^Gw^MR~N_IRMsGm-)IpZv1jfFqlf>L0ClODc4{${|DeGHyx7U>f&JjY1TFGAd4(Gt1?QHk*fEFA(y1X6U= za(P2gy>Tureaw8$?8~o(?_$#GXDNT$hGA7WEn`^qSz4wbNr%!Z{Am@_)b+ILaC&wq zz099p7Dz82Zn>dL50`@68`PU_7;`VmFKI4lUQ3?t^_y1DmRu{^2)V~v&$W*COsCAy z@3?;9@~v*Uvfo$U7EIqYS6Ct#F^z5u>$3q_01W&ZVBn-*TYO!cA0xXr%*sP+xBJ&_ z_cgR$HC(L^?X>uJT6{bHi?6Uds6F(V&KS}a`E^B8^`X)Re`!NN*D#lvJ7zy;|Irb! z2Qy}lr-yP@_~GBUVx~T{s>Q#m<(jcY>YT59M=*Wojr4qwr&l$E9%}JF)bi)@!_$vm zHD3MU@3VXzkA*sV{T;o5*4|ftc&+>}j<_N7(- z`|Wa~!VczV)x`3ZaIq;i-zZxS7nox6{d*cHy?{-Bb63k>jbzferYd|jJFhlgT?Hm~ z!R$TKASY8MQV=@ID8GNN7~91OxzC<}4gPU)*3L@#$Lr+qyqZMrN}{f68*AH=WWQfu zxO0=@4>?6U>y&?3E5q;xHN<~Xr`fqd^9djG$$ApQ8z@NqWRs#zrTHVJf;@jr((FkG}KZO8qYb|lGM3@qO7nbO51K_ai zvLx{fvQa#@J|T~XW<^W^_^-srMgDsN__Anj-_A@z?|N79?ysA9^q_MqAd>$TqM1wh zKwihwtJg`MUa>fgh*kW5gWzDvQZy$@cK-wNu{+=a#_=-wgAmBC;KrLD&dNiWG5;RE z@G;+fuFHl!qsh5p%tmu}zza*Ex@Sr^5U-stv;s}EnrIv2H1jx?U2%<}fm-j^oO{eJUSGsV0Q*%@3p=0kWu#0CuG zH1Qmb#U2A+5j4^-z{_CyQY*Yzn-20X!FECcK%oVq^`qor#awFIS^Fvb3rEIV{HeuL z+peWnfpSQo)InV_to0lyHg}xbG0KkYzN;cYaTJ?9kXnVGVrKAKo<)E!%&@SIs1sQV z@CB>HTK)KNWlq-_%E_Um8=#Nfp}rlXt|%v+?7o(D^pSo_#a@F_dNn=_mw z{!N$m!oaYW?+l1?N-z=e*!n=Xcph3|P;N07q0t9^@&-m9Nfs27;Gh(dAlLvFW|7tMH65C(-PVfj!*W^ng2xNuxD{{eHK4Zc>UnIOib zL_TV^+Tepb3(OerrZ9X{^MUg6iR49M4p zwYkHsXWLJ;2ekPoTkgsgih|pzIf`cTtF{Aqtq-2wqIGtGC+fkm<9}8 z#0TEl1?GWd290_=hn@CA3=$QvojL&@PW1At8MhM53}(>p1I(GNjehkde8I)fWB_YM zRWCog_thJPjkAeCGLE#7T?-Fj4kY34HQ3|34orZma+R delta 6638 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 U`LF1|tDH%IwfBf6K*d@A2NWzr%>V!Z diff --git a/Module/__pycache__/Main.cpython-312.pyc b/Module/__pycache__/Main.cpython-312.pyc index bdb6698db734f82ecb1fb70bf1b6cabd951f6234..a238f0f2b4d87f486c564070fa6abfe3298163f0 100644 GIT binary patch delta 20 acmew&^F@aHG%qg~0}!x$n5+Q@BV2>?ZO1(yH- delta 20 acmX?Jbi9cBG%qg~0}x2taBSqZu>=4?^#uI@ diff --git a/Utils/__pycache__/ThreadManager.cpython-312.pyc b/Utils/__pycache__/ThreadManager.cpython-312.pyc index d2834ff77d5c5c85c9699fca34b1fec960f618e8..460469374c2e079d32040529f48c08f7a3d820cf 100644 GIT binary patch delta 1117 zcmYLHT})eL7=F+HPkY)y4{d2FJ*6E`JDIMu%*~CElsQAd3+DyNxF8&ubcpkm{ix^2 z^kTAEW1BBo+x>*17qToSs26726$xw?8nfu84Vj51W49JGnPD;U`@q7NoacMa^Pcy4 z-tWu#>CDCV9N#O740!!ID~+R$uN}MNj=MBK0ZxEL~>;lM8*`!IQZ739TIk=sA_9%G zgLB3Jjx!{I7;bj9Z+0psr0Z*w?d<6W;uhR?vuvgf=fq5#zBQ@cw5L62nD?<*?m=^V zf=+QKdb9Y4=I^4it=TtOFlj^3C0#b@t8kqjqhE)2Z6h>=FVEu6LKvp(wde>K`z;y$ zYYX|gtDv){j5}C;`%3Y#|37MAdpHUPK?h6zK+aKFaf_krLBDkG95V=Ksi+-2>pbi* zLOA5al@eLEy3tFCr04OSJFA!GR=@k^$xLqb((LMo*PeX!dDMX}B!c}b{P3w$qm#Pf z#%+c~z)7H)KnsB|fma9|#Sm4D-GraRu)>cGk57#1D}phaIXy9zLH83c)Elj&PNv8x z7&J{oTr#1l_;1LJV>l1L*STi+O1(NGJXV79Jr9(QD%jB*$}7WaSm_Z_h#Pry_uVrJ`QKR z6GbDCm0Y2EBd&#v5|1%P0ZhYL&}iJ?$<1O^su?Cx7u^e8bcYi_Ln?#k3T1rFlT(l8 zY26N>B1{Jh5NNE1hWK5LsO&uHrmtq`s32ZWWmPVh)EW{%>`7!vV$lijW`Dw+WWsRFSfh20BJ!R{>k|Fhab0Zq6p1SfWP{LpC=PLDF} zFhZ3X@n7!|+)`DQvwG#h+Ve+0UM#LHTwPs!xi%U4{^m)UfWuWzcOaW22nafu2z=o> z2L`&&DP_h$iLQbMGf@|Fh>6V0)9nY(bo{AoeTkGOdT|B)mVLJ4C!v;BR)UQn=SPM3QG3O^` z$7k97L3T$vqbM>Q_u{wJc}h~Y|4H%Wcza7E9Z597wLREELQk?yS(^b diff --git a/script/__pycache__/ScriptManager.cpython-312.pyc b/script/__pycache__/ScriptManager.cpython-312.pyc index 6ba832bcc115bfb5ce06a5f98c5242d95056aaea..c536b668aa0be65a900dd63dd7f3259f4f1afb63 100644 GIT binary patch delta 10807 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