From 2254284625e31e1d4d83bbcaf68627fbfe916ac6 Mon Sep 17 00:00:00 2001 From: milk <53408947@qq.com> Date: Mon, 27 Oct 2025 21:44:16 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8E=89=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/misc.xml | 2 +- Module/DeviceInfo.py | 61 +++++- Module/IOSActivator.py | 192 ++++++++++-------- Module/__pycache__/DeviceInfo.cpython-312.pyc | Bin 24971 -> 27332 bytes .../__pycache__/FlaskService.cpython-312.pyc | Bin 38791 -> 38975 bytes Module/__pycache__/Main.cpython-312.pyc | Bin 3738 -> 3738 bytes Utils/ThreadManager.py | 23 ++- Utils/__pycache__/LogManager.cpython-312.pyc | Bin 14663 -> 14663 bytes .../__pycache__/ThreadManager.cpython-312.pyc | Bin 8837 -> 9747 bytes script/ScriptManager.py | 156 ++++++++------ .../__pycache__/ScriptManager.cpython-312.pyc | Bin 66235 -> 68734 bytes 11 files changed, 269 insertions(+), 165 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 20aef6e..a37b124 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/Module/DeviceInfo.py b/Module/DeviceInfo.py index 32516ff..4c39fae 100644 --- a/Module/DeviceInfo.py +++ b/Module/DeviceInfo.py @@ -161,8 +161,33 @@ class DeviceInfo: time.sleep(1) + def _wait_wda_ready_on_port(self, udid: str, local_port: int, total_timeout_sec: float = None) -> bool: + """在给定的本地映射端口上等待 /status 就绪。""" + import http.client, time + if total_timeout_sec is None: + total_timeout_sec = self.WDA_READY_TIMEOUT + deadline = _monotonic() + total_timeout_sec + attempt = 0 + while _monotonic() < deadline: + attempt += 1 + try: + conn = http.client.HTTPConnection("127.0.0.1", local_port, timeout=1.8) + conn.request("GET", "/status") + resp = conn.getresponse() + _ = resp.read(128) + code = getattr(resp, "status", 0) + ok = 200 <= code < 400 + print(f"[WDA] /status@{local_port} 第{attempt}次 code={code}, ok={ok} {udid}") + if ok: + return True + except Exception as e: + print(f"[WDA] /status@{local_port} 异常({attempt}): {e}") + time.sleep(0.5) + print(f"[WDA] /status@{local_port} 等待超时 {udid}") + return False + + def _add_device(self, udid: str): - method = "_add_device" print(f"[Add] 开始新增设备 {udid}") if not self._trusted(udid): @@ -177,8 +202,13 @@ class DeviceInfo: if not self._wda_http_status_ok_once(udid): if major > 17: + print("进入iOS17设备的分支") print(f"[WDA] iOS>17 调用 IOSActivator (port={wdaScreenPort})") - IOSActivator().activate(udid) + try: + IOSActivator().activate(udid) + print("wda启动完成") + except Exception as e: + print("错误信息:",e) else: print(f"[WDA] iOS<=17 启动 WDA app_start (port={wdaScreenPort})") dev = tidevice.Device(udid) @@ -196,7 +226,7 @@ class DeviceInfo: if not (w and h and s): # 再做几次快速重试(带超时) for i in range(4): - print(f"[Screen] 第{i+1}次获取失败, 重试中... {udid}") + 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: @@ -254,18 +284,24 @@ class DeviceInfo: 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() + """只做一次 /status 探测。任何异常都返回 False,不让外层炸掉。""" + tmp_port = None proc = None try: + tmp_port = self._alloc_port() # 这里可能抛异常 print(f"[WDA] 启动临时 iproxy 以检测 /status {udid}") proc = self._spawn_iproxy(udid, local_port=tmp_port, remote_port=wdaScreenPort) + if not proc: + print("[WDA] 启动临时 iproxy 失败") + return False if not self._wait_until_listening(tmp_port, 3.0): print(f"[WDA] 临时端口未监听 {tmp_port}") - self._release_port(tmp_port) return False + + # 最多两次快速探测 for i in (1, 2): try: + import http.client conn = http.client.HTTPConnection("127.0.0.1", tmp_port, timeout=timeout_sec) conn.request("GET", "/status") resp = conn.getresponse() @@ -275,16 +311,21 @@ class DeviceInfo: 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) + time.sleep(0.25) return False + + except Exception as e: + import traceback + print(f"[WDA][probe] 异常:{e}\n{traceback.format_exc()}") + return False + finally: if proc: self._kill(proc) - # 无论成功失败,都释放临时端口占用 - self._release_port(tmp_port) + if tmp_port is not None: + self._release_port(tmp_port) def _wait_wda_ready_http(self, udid: str, total_timeout_sec: float) -> bool: print(f"[WDA] 等待 WDA Ready (超时 {total_timeout_sec}s) {udid}") diff --git a/Module/IOSActivator.py b/Module/IOSActivator.py index 8651f1d..5c009d3 100644 --- a/Module/IOSActivator.py +++ b/Module/IOSActivator.py @@ -34,120 +34,138 @@ class IOSActivator: # =============== 公共入口 =============== def activate( - self, - udid: str, - wda_bundle_id: Optional[str] = WdaAppBundleId, - ready_timeout_sec: float = 120.0, - mount_retries: int = 3, - backoff_seconds: float = 2.0, - rsd_probe_retries: int = 5, - rsd_probe_delay_sec: float = 3.0, - pre_mount_first: bool = True, - keep_tunnel: bool = False, # << 新增:默认 False,一次性用完就关 - broad_cleanup_on_exit: bool = True, # << 退出时顺带清理所有 pmd3 残留网卡 + self, + udid: str, + wda_bundle_id: str = WdaAppBundleId, + ready_timeout_sec: float = 60.0, + pre_mount_first: bool = True, + mount_retries: int = 2, + backoff_seconds: float = 1.5, + keep_tunnel: bool = False, + broad_cleanup_on_exit: bool = True, ) -> str: """ - 执行:挂镜像(可选) -> 开隧道 -> (等待 RSD 就绪)-> 启动 WDA - - 默认 keep_tunnel=False:WDA 启动后关闭隧道(避免虚拟网卡常驻) - - keep_tunnel=True:让隧道常驻,交由 atexit/signal 或上层调用 stop_tunnel() 清理 + Windows 简版:不读任何 tunneld 日志,也不做 RSD 解析。 + 逻辑:先探活 -> 开隧道 -> 直接用 HTTP 隧道端口反复尝试启动 WDA -> 探活成功即返回。 """ + import time, ctypes, traceback + if not udid or not isinstance(udid, str): raise ValueError("udid is required and must be a non-empty string") - print(f"[activate] UDID = {udid}") - self._ensure_exit_hooks(broad_cleanup_on_exit=broad_cleanup_on_exit) + print(f"[activate] UDID={udid}", flush=True) - # 管理员检测(Windows 清理网卡需要) - if os.name == "nt": - import ctypes + # —— 管理员提示(Windows 清理虚拟网卡常用)—— + try: + if ctypes.windll.shell32.IsUserAnAdmin() == 0: + print("[⚠] 未以管理员运行:若需要移除虚拟网卡,可能失败。", flush=True) + except Exception: + pass + + # —— 退出钩子(可选)—— + try: + self._ensure_exit_hooks(broad_cleanup_on_exit=broad_cleanup_on_exit) # type: ignore[attr-defined] + except Exception as e: + print(f"[activate] _ensure_exit_hooks warn: {e}", flush=True) + + # —— 小工具:探活 WDA —— # + def _wda_alive(timeout: float = 2.0) -> bool: try: - is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 + if hasattr(self, "_wda_alive_now"): + return bool(self._wda_alive_now(udid, timeout=timeout)) # type: ignore[attr-defined] + if hasattr(self, "_wda_client"): + cli = self._wda_client(udid) # type: ignore[attr-defined] + if hasattr(cli, "wait_ready"): + return bool(cli.wait_ready(timeout=timeout)) except Exception: - is_admin = False - if not is_admin: - print("[⚠] 未以管理员运行:若需要移除虚拟网卡,可能失败。") + return False + return False - import time as _t - start_ts = _t.time() + # 0) 快路径:WDA 已活 + if _wda_alive(2.0): + print("[activate] WDA already alive, skip launching.", flush=True) + return "WDA already alive" - # 1) 预挂载 - if pre_mount_first: + # 1) 预挂载(失败不致命) + if pre_mount_first and hasattr(self, "_auto_mount_developer_disk"): try: - self._auto_mount_developer_disk(udid, retries=mount_retries, backoff_seconds=backoff_seconds) - _t.sleep(2) + self._auto_mount_developer_disk(udid, retries=mount_retries, + backoff_seconds=backoff_seconds) # type: ignore[attr-defined] + time.sleep(1.5) except Exception as e: - print(f"[activate] 预挂载失败(继续尝试开隧道后再挂载一次):{e}") + print(f"[activate] 预挂载失败(继续):{e}", flush=True) - # 2) 启动 tunneld - http_host = http_port = rsd_host = rsd_port = None - iface_names: Set[str] = set() - proc, port = self._start_tunneld(udid) - self._live_procs[udid] = proc - self._live_ifaces[udid] = iface_names + # 2) 开隧道(关键:拿到 HTTP 端口即可;不读取任何 stdout/stderr) + proc = None + http_host, http_port = "127.0.0.1", None + try: + ret = self._start_tunneld(udid) # type: ignore[attr-defined] + if isinstance(ret, tuple): + proc, http_port = ret[0], ret[1] + else: + proc = ret + if http_port is None: + # 若你的 _start_tunneld 固定端口,可在这里写死(例如 8100/某自定义端口) + raise RuntimeError("未获取到 HTTP 隧道端口(_start_tunneld 未返回端口)") + except Exception: + # 即便开隧道失败,也再探活一次(可能本来就活) + if _wda_alive(2.0): + print("[activate] WDA already alive (tunnel start failed but OK).", flush=True) + return "WDA already alive" + raise - captured: List[str] = [] - wda_started = False - mount_done = pre_mount_first + print(f"[tunneld] HTTP tunnel at {http_host}:{http_port}", flush=True) + + # 3) 直接用 HTTP 隧道反复尝试启动 WDA + 探活 + deadline = time.time() + (ready_timeout_sec if ready_timeout_sec > 0 else 60.0) + launched = False try: - assert proc.stdout is not None - for line in proc.stdout: - captured.append(line) - print(f"[tunneld] {line}", end="") - - # 捕获虚拟网卡名 - for m in self.IFACE_RE.finditer(line): - iface_names.add(m.group(1)) - - if proc.poll() is not None: + while time.time() < deadline: + # 已活则成功返回 + if _wda_alive(1.5): + print("[activate] WDA detected alive.", flush=True) + launched = True break - # 捕获 HTTP 网关端口 - if http_port is None: - m = self.HTTP_RE.search(line) - if m: - http_host, http_port = m.group(1), m.group(2) - print(f"[tunneld] Tunnel API: {http_host}:{http_port}") + # 尝试发起一次 HTTP 启动(失败就下一轮重试) + try: + if hasattr(self, "_launch_wda_via_http_tunnel"): + self._launch_wda_via_http_tunnel( # type: ignore[attr-defined] + bundle_id=wda_bundle_id, + http_host=http_host, + http_port=str(http_port), + udid=udid, + ) + except Exception as e: + # 仅打印,不中断;下一次循环再试 + print(f"[activate] _launch_wda_via_http_tunnel error: {e}", flush=True) - # 捕获 RSD(仅识别当前 UDID 的行) - if not self._line_is_for_udid(line, udid): - continue - m = self.RSD_CREATED_RE.search(line) or self.RSD_FALLBACK_RE.search(line) - if m and not rsd_host and not rsd_port: - rsd_host, rsd_port = m.group(1), m.group(2) - print(f"[tunneld] Device-level tunnel ready (RSD {rsd_host}:{rsd_port}).") + # 启动后给一点时间让 WDA ready + for _ in range(3): + if _wda_alive(1.0): + launched = True + break + time.sleep(0.5) - # 启动 WDA - if (not wda_started) and wda_bundle_id and (rsd_host and rsd_port): - if not mount_done: - self._auto_mount_developer_disk(udid, retries=mount_retries, backoff_seconds=backoff_seconds) - _t.sleep(2) - mount_done = True - - if self._wait_for_rsd_ready(rsd_host, rsd_port, retries=rsd_probe_retries, delay=rsd_probe_delay_sec): - self._launch_wda_via_rsd(bundle_id=wda_bundle_id, rsd_host=rsd_host, rsd_port=rsd_port, udid=udid) - wda_started = True - elif http_host and http_port: - self._launch_wda_via_http_tunnel(bundle_id=wda_bundle_id, http_host=http_host, http_port=http_port, udid=udid) - wda_started = True - else: - raise RuntimeError("No valid tunnel endpoint for WDA.") - - # 超时保护 - if (not wda_started) and ready_timeout_sec > 0 and (_t.time() - start_ts > ready_timeout_sec): - print(f"[tunneld] Timeout waiting for device tunnel ({ready_timeout_sec}s). Aborting.") + if launched: break - # 结束/收尾 - out = "".join(captured) + time.sleep(1.0) # 下一轮重试 + + if not launched: + raise RuntimeError(f"WDA not ready within {ready_timeout_sec}s via HTTP tunnel") + + print("[activate] Done.", flush=True) + return f"http://{http_host}:{http_port}" finally: if not keep_tunnel: - # 一次性模式:WDA 已启动后就关闭隧道并清理网卡 - self.stop_tunnel(udid, broad_cleanup=broad_cleanup_on_exit) + try: + self.stop_tunnel(udid, broad_cleanup=broad_cleanup_on_exit) # type: ignore[attr-defined] + except Exception as e: + print(f"[activate] stop_tunnel warn: {e}", flush=True) - print("[activate] Done.") - return out # =============== 外部可显式调用的清理 =============== def stop_tunnel(self, udid: str, broad_cleanup: bool = True): diff --git a/Module/__pycache__/DeviceInfo.cpython-312.pyc b/Module/__pycache__/DeviceInfo.cpython-312.pyc index fd0ed3e69497bf5fe791657257248eede2890269..b77b316005a84557e2e1a48970f3153562a9df89 100644 GIT binary patch delta 5158 zcma)A3s_XwwLbftc`(d_fjRTyd0b#5t;suoOP-;+o5Dfz1j1dGz&kRL$#zAWm zj8w5ZiA0l>G^WKs;}~Kx`HYEqN~&##ND7^p+w{f(&3C_>G2u25@9n*7A3$i@CfR)R z@3r?{Yn{FK*=z0paQGec?ng*psAholE)dKq2rPT6{A%Gjv~QC&oM9wXbY2&a8aOT5Om1oqTz?E zMr&mKVmwEw*N1R1Fdlt(eAZZ)go}l-7;es8ePf&ndSbaa>l|3pT#!xRjVv!7$TrAb%j4{fm3)-+V@wDAsqxWncE*-_VMwKY4c?AF>^5!uqIqJWx#-&ZSWiaJhl zh@%@Qmq@6QOYCIY6i)nSiB{xd98w%tqECgTxug#1w1eI$!714R9{87>QjXc9q$rnE zMma?|sATbA7Rz!XC(VibB)#NeP+TF-5PWPYi$9SoM2Lb$hB#W}k~wAgqLg)WvR*W$ z80yeWN)+XkaiJW#>q5@So$^|GH@zd0qIM%*j3`b)X3<2~(ui^?9NKA1PU)miQk=>u z1q^e_0o6_gV7L>Vq@E6)ej3a~!J}4Lgp8sor?}pL-%5(bCpLw;zqL-(@zOtd*9pEv zGrn5&@svY1J%;*@9ZA-OPPd3o;v; zmx9y4Q$1tnpA5WsV(i4h(Wm+XPkBbWx&xkrW4&hrN1wayeRS+MhXQXrWJNhxG~k3RBQq;)A^c^G{q71W|C4(f`K43G#m^MovwlJf9^AwQ zzYum=(`p;) ztW6G{oCv;*fB^tcEc>?=)ud%1P!C{M@x?@5Phby$y#Rg@aUxjzLpW;<*HG7F^-F6U z4r}912i}NnY)wLZ?U`F({YhP|wWMjAE$v_E zoB{{-6|hll)L4jVL>}#vCyba9hE0nHO^bbsm}{CCoa38o8Pr(Z?I9x(ns(_mmEmwh zSH*{_#1Vz2bJwBL_M#vySC*m?ebn*2$Mz2EGY0h;p84KAzVeMeea2P&rcTj_nmw*K zrs&$xT{5Ij1I=N5@}NHXs@@D^tl@acv68M@yrvUf)5mr>x+{8iUDap6kk-&u@JDS@ zw`EA{PU@tuskL3I?mgWNo&-;er_S3tzz#?TtiJUdedU{cn<{+wRr)Hn`gB!8>gqpf zv?Eb-f(*s#f+3Vr-(GNZ)uC0L_KthUW2i7AS;JLT;&?12Q+2Ak_H;G$q<^Q7FiA_^lKT_Qub{8-K*&zs4_6 z(hhP~h2o@fdV<+_H;y^65&EYi)5YLj32|}nI~h*0qW}(8!7RVhX)Q$IrxTKp48NUV zNtmTDE73Hqo7JztrxT)ZY+?#S!{f`sSX`NCK}viq@yAGo^VezQGm{U)|C^YO)HpRM zo(b=Zz~xD1q`|JFs4&(U!fDBAhr|ZS2;W(niu8B&?j+?S$Y-$dvg8!0(J2G1NbF9w zpeWp*ybML-FOu`aw0$wXuwA6MJ-8RUrye9b7s4M4hN(d{QF04*yN9lXvW% zN_mNEaHDyBEF^mB#+xSt4?SOZZ+S-6jThetoO%TEU!eWr(Z|l;&YZda=Ch;w&wTOv3H*imxQ2fe2BuPvB9C`N3qLpSv{*

oSMb7C65?L+-muuX~`z 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 R1TMuqJpN-F*xBKT@DHVMa@PO= diff --git a/Module/__pycache__/Main.cpython-312.pyc b/Module/__pycache__/Main.cpython-312.pyc index 9e23e1d7d70d829bf3c644c830f85ab2c637285d..993b37132f6af8c518b6e69ae2baeaacf281d2d9 100644 GIT binary patch delta 20 acmbOwJ4=@PG%qg~0}wR&{olyl&j$cE*9DLO delta 20 acmbOwJ4=@PG%qg~0}!-K`L~g~pAP^ziv`#K diff --git a/Utils/ThreadManager.py b/Utils/ThreadManager.py index 6d2a802..632cabe 100644 --- a/Utils/ThreadManager.py +++ b/Utils/ThreadManager.py @@ -94,12 +94,20 @@ class ThreadManager: except Exception as e: LogManager.method_error(f"[{udid}] 设置停止事件失败: {e}", "task") - # 🔹 不阻塞主线程 def _wait_stop(): + # 先给 1 秒高频检查机会(很多 I/O 点会在这个窗口立刻感知到) + t0 = time.time() + while time.time() - t0 < 1.0 and thread.is_alive(): + time.sleep(0.05) + + # 再进入原有的 join 窗口 thread.join(timeout=stop_timeout) if thread.is_alive(): LogManager.method_info(f"[{udid}] 协作超时 -> 尝试强杀", "task") - _async_raise(tid) + try: + _async_raise(tid) # 兜底:依然保留你的策略 + except Exception as e: + LogManager.method_error(f"[{udid}] 强杀触发失败: {e}", "task") thread.join(timeout=kill_timeout) if not thread.is_alive(): @@ -136,10 +144,15 @@ class ThreadManager: with ThreadPoolExecutor(max_workers=4) as executor: futures = {executor.submit(cls.stop, udid): udid for udid in ids} for future in as_completed(futures): - code, msg = future.result() + udid = futures[future] + try: + code, msg = future.result() + except Exception as e: + LogManager.method_error(f"[{udid}] stop 调用异常: {e}", "task") + failed.append(udid) + continue if code != 200: - failed.append(futures[future]) - + failed.append(udid) if failed: return 207, f"部分任务停止失败: {failed}" return 200, "全部停止请求已提交" \ No newline at end of file diff --git a/Utils/__pycache__/LogManager.cpython-312.pyc b/Utils/__pycache__/LogManager.cpython-312.pyc index 7d1bd6e1d99c3365ff1897afbd8ac4c2004237f2..5063933152f9f0b88302bb17a462bca5b166d6c1 100644 GIT binary patch delta 20 acmX?Jbi9cBG%qg~0}yok{olxKV+jCCLF7(Vyj-rL@m0=>PLLQCuAOQ=-@6jay-h%lYV+;kvQ!z#rOzIR*2Ev=X> zW;S8da@=5XCYa5MbIC9)i&^}kI2V%rDGNr^@WW+ey8S92i3XFo_uRrjXW}J1=eh5B z&vV{;&UsExRO0IRUyMdQf~S1flY=RiEAhQ?)cxV}acwM>hZBT!Di;?Bxxn4RCaV_m z%q9<@421`eB}n)luQLSx9~1oOeVD>Swa^GD+kns1p!f%ycDvfjtz>t_3EO;CFldDwG|hr0}AkcUD% z@)=_sp&++(bWDYkQtw?gle6nkE*mw&o9NH+@$6E3!eP)ZR0rk9eM zEsN0w7S!VPqAd907Xq0`SstKb(>P<(X#Lb#bWXGM+v<@A1*l_G&v2~8Vq8X|zUDwo zC0eSi0*2oeH&~N^?3E|4{rKm(;}0%=J$WiL`PG#N-(M(}@g;HVTX9MeBq-?w)&WE` z%^io@6&-mO30Mh81W1WWJ^?#{3;?GtLi_TRWb&R#;3WbjIA1bzL_(JFW~p?a8>qJi z_W#jw^oL36O9`c5+sTa+LQ(Q(kC8tdz zN=u7hEC&RS{Z_|F%|B1$dn;i}DZewK*s~4{>lani;_-Qa_Oj5DIX9*3YFtETG|$ zpKIp9uHAdUH34lv=hu3;7B~l9swdq~H=7{sna8hLbW+b+4YPb*rF)}!;& zaBAuO^bYhM-$+xakq&6OPzLg;HxiMBv%W6CgG+IcPVHv=8ZfcO&#Q->T`KsZLR35O z$M~L;U4NF&!G{#;*&T~J^)-KKpadrsF|@glHgt91u${|t zLFh0!?R#afucK3bF!bU5GlP@YKA60EwOCf-pa(Ycj(r8(nY1Ibj919&MmX=`eQk$) z$`;a3WFwr?#smTjrUYU2LZ4)^>{z;QsPZY z`TrIjYK1^qh*GCr@5 zBA|nXL*}8zq2x=Y)8tjno}z&%*<`*{0H;YwAe_{%6yXbLjgp7ZMgr=!O0tyw*q)wD Twcw)kvV>z4RWOYRsxtoql3CX> delta 1264 zcmZvbUuauZ9LLYOH}|GVlcxF8B+b7iO}Z{y*EY%0(oNT`>l8*iaqWWzAIx&ItzFX8 zdsC{A!W4!^%9`yc>!@R$_z<1b)`A=2gLZopAH)eE>_K5Iz6j|mR5}EIzng+=?!x_? z-#NeU`Q7h5f7sx~;|)I=jd}u4;P#Vq)3)V?D+YRY<)$Gc&`xHuw9x%5YUzMDVHxNZ zL1%?`^E*n&N8(jd;Kw0)_9<{*&|5p>6gB)6vr8lQWpiAUecb&2 zY(Ze%O@8J#I5_P|_Ipz=i`!3(=qj4=?AV0d0u$sqgbm?BG$6VVUPKTO7UdAehY$x5 zeuNI;11Px2>`!~2=`{B40{*^0$_5L&|Hj`YYDp~ih1h;0S` zXSLmX%);NZ2*=#okuX;ga|!uuDpwJuL@JY=2y=2LsHU0#Ywvu#e*2R@zPhu%{O;PF zuh-{_kG}mhY?Qm$Bj>=rC@8}EnpLz_v#OeI#qJ)26LFLcH*eOr8+G6V@RyIh>hjU> z%B1T(PCSf?{p=slL2&@XBkV=*AdRpOysb31vgGwqJuVp!?`AhE`z~q0-;9_S*`nKH ze;xL6MmtlN*Gl~P9#{=J>&bJh*l4rmg}m4dv;I3dvk~^wuI89V67t*vxz3&8hRNGv zlA|Qa&1j}ckVK%sH1tPdmnoTbzQvD{X&TnD8-6>S@QQygPzZz~qKt+rqB3 zgKvb6gRml;8q6lotEaM3pG+qdC6mgX%S!TY$jUFVFN1DRuPQu~&L(#LK7u(d;x)Dw z{KnoUw9oTZ0!=BdHs7b9Vap+_3C1>gBATllMAcL5mr$UuUWm?*EC#QQRtbcq_>y%g zx#aw;?=f~&^#%u#+=gH90K#V3gfTjn<1E&i?7-_FClKn@LX|8b)Jb?2w(DNVWTmNe V>aaYy-8{GF+r-Le*w*IAMXGF diff --git a/script/ScriptManager.py b/script/ScriptManager.py index 9148755..78de124 100644 --- a/script/ScriptManager.py +++ b/script/ScriptManager.py @@ -49,6 +49,29 @@ class ScriptManager(): self.initialized = True # 标记已初始化 + # 放在类里或公共工具模块里均可 + def interruptible_wait(self,event: threading.Event, total: float, step: float = 0.2) -> bool: + """ + 等待 total 秒,但每 step 秒检查一次停止信号。 + 若在等待期间 event 被置位,立刻返回 True;否则到点返回 False。 + """ + deadline = time.time() + total + while time.time() < deadline: + if event.is_set(): + return True + event.wait(timeout=min(step, max(0, deadline - time.time()))) + return event.is_set() + + def interruptible_sleep(self,event: threading.Event, seconds: float, step: float = 0.2) -> bool: + """语义同上;返回 True 表示期间接到停止信号。""" + return self.interruptible_wait(event, seconds, step) + + def check_stop(self,event: threading.Event, tag: str = ""): + """在关键点快速失败,保持调用栈整洁(不改变业务路径,只是早退出)。""" + if event.is_set(): + raise RuntimeError(f"stop-requested:{tag}") + + # ========= 评论逻辑 ========= def comment_flow(self, filePath, session, udid, recomend_cx, recomend_cy): """评论一条龙:点评论框->输入->发送->返回""" @@ -57,9 +80,7 @@ class ScriptManager(): if not coord: return # 没检测到评论按钮就拉倒 - print(11111111111) cx, cy = coord[0] # ✅ 注意这里取第一个点 - session.click(int(cx / 3), int(cy / 3)) print(f"点击评论的坐标:{int(cx / 3)}, {int(cy / 3)}") @@ -412,7 +433,6 @@ class ScriptManager(): """ def safe_greetNewFollowers(self, udid, needReply, needTranslate, isComment, event): - retries = 0 while not event.is_set(): try: @@ -428,9 +448,11 @@ class ScriptManager(): break LogManager.method_error("greetNewFollowers 重试次数耗尽,任务终止", "关注打招呼", udid) - # 关注打招呼以及回复主播消息 def greetNewFollowers(self, udid, needReply, needTranslate, isComment, event): + if self.check_stop(event, "init"): # [ADD] + return + client = wda.USBClient(udid, ev.wdaFunctionPort) session = client.session() @@ -441,12 +463,18 @@ class ScriptManager(): # 先关闭Tik Tok ControlUtils.closeTikTok(session, udid) - event.wait(timeout=1) + if self.interruptible_sleep(event, 1): # [ADD] 可中断等待 + return + + if self.check_stop(event, "after-close-app"): # [ADD] + return # 重新打开Tik Tok ControlUtils.openTikTok(session, udid) - event.wait(timeout=3) + if self.interruptible_sleep(event, 3): # [ADD] + return LogManager.method_info(f"重启tiktok", "关注打招呼", udid) + # 设置查找深度 session.appium_settings({"snapshotMaxDepth": 15}) @@ -457,10 +485,12 @@ class ScriptManager(): def goBack(count): for i in range(count): LogManager.method_info(f"返回上一步", "关注打招呼", udid) - + if self.check_stop(event, f"goBack-{i + 1}/{count}"): # [ADD] + return session.appium_settings({"snapshotMaxDepth": 15}) ControlUtils.clickBack(session) - event.wait(timeout=2) + if self.interruptible_sleep(event, 2): # [ADD] + return LogManager.method_info(f"循环条件1:{not event.is_set()}", "关注打招呼", udid) LogManager.method_info(f"循环条件2:{len(anchorList) > 0}", "关注打招呼", udid) @@ -469,13 +499,15 @@ class ScriptManager(): # 循环条件。1、 循环关闭 2、 数据处理完毕 while not event.is_set(): + if self.check_stop(event, "loop-top"): # [ADD] + return + LogManager.method_info("=== 外层 while 新一轮 ===", "关注打招呼", udid) if event.is_set(): break # 获取一个主播, LogManager.method_info(f"开始获取数据", "关注打招呼", udid) - # 获取一个主播, result = AiUtils.peek_aclist_first() LogManager.method_info(f"数据是:{result}", "关注打招呼", udid) @@ -493,17 +525,21 @@ class ScriptManager(): if not anchor: LogManager.method_info(f"数据库中的数据不足", "关注打招呼", udid) + # 你原来的写法:等待完成就 continue;中途被打断就 return if not self.interruptible_sleep(event, 30): continue + return # [ADD] 被打断则退出 aid = anchor.get("anchorId", "") anchorCountry = anchor.get("country", "") LogManager.method_info(f"主播的数据,用户名:{aid},国家:{anchorCountry}", "关注打招呼", udid) + if self.check_stop(event, "before-search"): # [ADD] + return + # 点击搜索按钮 ControlUtils.clickSearch(session) - LogManager.method_info(f"点击搜索按钮", "关注打招呼", udid) # 强制刷新session @@ -515,20 +551,21 @@ class ScriptManager(): # 如果找到了输入框,就点击并且输入内容 if input.exists: input.click() - # 稍作停顿 - event.wait(timeout=0.5) + # 稍作停顿(用你的可中断等待) + if self.interruptible_sleep(event, 0.5): # [ADD] + return else: print(f"找不到输入框") input = session.xpath('//XCUIElementTypeSearchField') if input.exists: input.clear_text() - event.wait(timeout=1) + if self.interruptible_sleep(event, 1): # [ADD] + return # 输入主播id input.set_text(f"{aid or '暂无数据'}\n") # 定位 "关注" 按钮 通过关注按钮的位置点击主播首页 - session.appium_settings({"snapshotMaxDepth": 25}) try: @@ -543,15 +580,22 @@ class ScriptManager(): session.appium_settings({"snapshotMaxDepth": 15}) continue - event.wait(timeout=2) + if self.interruptible_sleep(event, 2): # [ADD] + return + # 找到并点击第一个视频 cellClickResult, workCount = ControlUtils.clickFirstVideoFromDetailPage(session) - LogManager.method_info(f"点击第一个视频", "关注打招呼", udid) - event.wait(timeout=2) + + if self.interruptible_sleep(event, 2): # [ADD] + return # 观看主播视频 def viewAnchorVideo(workCount): + + if self.check_stop(event, "viewVideo-enter"): # [ADD] + return + print("开始查看视频,并且重新调整查询深度") session.appium_settings({"snapshotMaxDepth": 5}) @@ -566,12 +610,16 @@ class ScriptManager(): LogManager.method_info("停止脚本中", method="task") if event.is_set(): break - event.wait(timeout=1) + if self.interruptible_sleep(event, 1): # [ADD] + return LogManager.method_info("停止脚本成功", method="task") - img = client.screenshot() - event.wait(timeout=1) - # filePath = f"resources/{udid}/bgv.png" + if self.check_stop(event, "before-screenshot"): # [ADD] + return + + img = client.screenshot() + if self.interruptible_sleep(event, 1): # [ADD] + return base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) # 当前脚本目录的上一级 filePath = os.path.join(base_dir, "resources", udid, "bgv.png") @@ -582,13 +630,12 @@ class ScriptManager(): img.save(filePath) LogManager.method_info("保存屏幕图像成功", "关注打招呼", udid) - event.wait(timeout=2) + if self.interruptible_sleep(event, 2): # [ADD] + return + # 查找add图标 r = ControlUtils.clickLike(session, udid) - # 点赞成功。 - # if r == True: - count -= 1 LogManager.method_info("准备停止脚本", method="task") # 随机看视频 15~30秒 @@ -596,7 +643,8 @@ class ScriptManager(): LogManager.method_info("停止脚本中", method="task") if event.is_set(): break - event.wait(timeout=1) + if self.interruptible_sleep(event, 1): # [ADD] + return LogManager.method_info("停止脚本成功", method="task") # 使用OCR进行评论 @@ -617,16 +665,20 @@ class ScriptManager(): # 观看主播视频 LogManager.method_info("去查看主播视频", "关注打招呼", udid) viewAnchorVideo(workCount) - event.wait(timeout=3) + if self.interruptible_sleep(event, 3): # [ADD] + return LogManager.method_info("视频看完了,重置试图查询深度", "关注打招呼", udid) session.appium_settings({"snapshotMaxDepth": 25}) - event.wait(timeout=0.5) + if self.interruptible_sleep(event, 0.5): # [ADD] + return # 向上滑动 ControlUtils.swipe_down(udid) - event.wait(timeout=2) + if self.interruptible_sleep(event, 2): # [ADD] + return msgButton = AiUtils.getSendMesageButton(session) - event.wait(timeout=2) + if self.interruptible_sleep(event, 2): # [ADD] + return if msgButton.exists: # 进入聊天页面 @@ -640,7 +692,8 @@ class ScriptManager(): session.appium_settings({"snapshotMaxDepth": 15}) continue - event.wait(timeout=3) + if self.interruptible_sleep(event, 3): # [ADD] + return # 查找聊天界面中的输入框节点 chatInput = session.xpath("//TextView") if chatInput.exists: @@ -653,14 +706,11 @@ class ScriptManager(): # 准备打招呼的文案 text = random.choice(ev.prologueList) - # text = "hello" LogManager.method_info(f"取出打招呼的数据,{text}, 判断是否需要翻译:{needTranslate}", "关注打招呼", udid) - # isContainChniese = AiUtils.contains_chinese(text) if needTranslate: - # 翻译成主播国家的语言 LogManager.method_info(f"需要翻译:{text},参数为:国家为{anchorCountry}, 即将进行翻译", "关注打招呼", udid) msg = Requester.translation(text, anchorCountry) @@ -674,10 +724,10 @@ class ScriptManager(): if chatInput.exists: chatInput.click() chatInput.set_text(f"{msg or '暂无数据'}\n") - event.wait(timeout=2) - # 发送消息 - # input.set_text(f"{aid or '暂无数据'}\n") - event.wait(timeout=1) + if self.interruptible_sleep(event, 2): # [ADD] + return + if self.interruptible_sleep(event, 1): # [ADD] + return else: print("无法发送信息") LogManager.method_info(f"给主播{aid} 发送消息失败", "关注打招呼", udid) @@ -685,27 +735,6 @@ class ScriptManager(): # 接着下一个主播 goBack(1) - # 点击关注按钮 - # followButton = AiUtils.getFollowButton(session).get(timeout=5) - # if followButton is not None: - # # LogManager.method_info("找到关注按钮了", "关注打招呼", udid) - # # followButton.click() - # x, y, w, h = followButton.bounds - # cx = int(x + w / 2) - # cy = int(y + h / 2) - # # 随机偏移 ±5 px(可自己改范围) - # cx += random.randint(-5, 5) - # cy += random.randint(-5, 5) - # - # session.click(cx, cy) - # - # else: - # LogManager.method_info("没找到关注按钮", "关注打招呼", udid) - # time.sleep(1) - # goBack(4) - # session.appium_settings({"snapshotMaxDepth": 15}) - # continue - session.appium_settings({"snapshotMaxDepth": 15}) goBack(3) @@ -718,7 +747,8 @@ class ScriptManager(): # 设置查找深度 session.appium_settings({"snapshotMaxDepth": 15}) - event.wait(timeout=2) + if self.interruptible_sleep(event, 2): # [ADD] + return print("即将要回复消息") LogManager.method_info("即将要回复消息", "关注打招呼", udid) @@ -735,9 +765,11 @@ class ScriptManager(): homeButton.click() else: ControlUtils.closeTikTok(session, udid) - event.wait(timeout=2) + if self.interruptible_sleep(event, 2): # [ADD] + return ControlUtils.openTikTok(session, udid) - event.wait(timeout=3) + if self.interruptible_sleep(event, 3): # [ADD] + return print("重新创建wda会话 防止wda会话失效") client = wda.USBClient(udid, ev.wdaFunctionPort) diff --git a/script/__pycache__/ScriptManager.cpython-312.pyc b/script/__pycache__/ScriptManager.cpython-312.pyc index 78f3251be854ab035111a113ae1d039b57c1c9d0..8a385046201e05b3a53ae70ad0f5cc92cb0c9b60 100644 GIT binary patch delta 12987 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