继续优化批量停止脚本
This commit is contained in:
28
.idea/workspace.xml
generated
28
.idea/workspace.xml
generated
@@ -5,7 +5,9 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成">
|
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成">
|
||||||
<change beforePath="$PROJECT_DIR$/Utils/ThreadManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/ThreadManager.py" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/resources/bb5886c82b356593c2b3d917d578862cf0abf9c0/bgv.png" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/Module/DeviceInfo.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/DeviceInfo.py" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/script/ScriptManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/script/ScriptManager.py" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/script/ScriptManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/script/ScriptManager.py" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
@@ -57,6 +59,7 @@
|
|||||||
"Python.123.executor": "Run",
|
"Python.123.executor": "Run",
|
||||||
"Python.Main.executor": "Run",
|
"Python.Main.executor": "Run",
|
||||||
"Python.Test.executor": "Run",
|
"Python.Test.executor": "Run",
|
||||||
|
"Python.test (1).executor": "Run",
|
||||||
"Python.test.executor": "Run",
|
"Python.test.executor": "Run",
|
||||||
"Python.tidevice_entry.executor": "Run",
|
"Python.tidevice_entry.executor": "Run",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
@@ -179,6 +182,28 @@
|
|||||||
<option name="INPUT_FILE" value="" />
|
<option name="INPUT_FILE" value="" />
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
|
<configuration name="test (1)" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||||
|
<module name="iOSAI" />
|
||||||
|
<option name="ENV_FILES" value="" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
<envs>
|
||||||
|
<env name="PYTHONUNBUFFERED" value="1" />
|
||||||
|
</envs>
|
||||||
|
<option name="SDK_HOME" value="" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||||
|
<option name="IS_MODULE_SDK" value="true" />
|
||||||
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/test.py" />
|
||||||
|
<option name="PARAMETERS" value="" />
|
||||||
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
<option name="MODULE_MODE" value="false" />
|
||||||
|
<option name="REDIRECT_INPUT" value="false" />
|
||||||
|
<option name="INPUT_FILE" value="" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
<configuration name="test" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
<configuration name="test" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||||
<module name="iOSAI" />
|
<module name="iOSAI" />
|
||||||
<option name="ENV_FILES" value="" />
|
<option name="ENV_FILES" value="" />
|
||||||
@@ -204,6 +229,7 @@
|
|||||||
</configuration>
|
</configuration>
|
||||||
<recent_temporary>
|
<recent_temporary>
|
||||||
<list>
|
<list>
|
||||||
|
<item itemvalue="Python.test (1)" />
|
||||||
<item itemvalue="Python.test" />
|
<item itemvalue="Python.test" />
|
||||||
<item itemvalue="Python.123" />
|
<item itemvalue="Python.123" />
|
||||||
<item itemvalue="Python.Test" />
|
<item itemvalue="Python.Test" />
|
||||||
|
|||||||
@@ -317,59 +317,86 @@ class Deviceinfo(object):
|
|||||||
|
|
||||||
# 检测usb是否超时
|
# 检测usb是否超时
|
||||||
def _usb_client_with_timeout(self,udid: str, timeout: 8):
|
def _usb_client_with_timeout(self,udid: str, timeout: 8):
|
||||||
|
LogManager.info(f"[CONNECT FLOW] 即将创建 USBClient(超时 {timeout}s)", udid)
|
||||||
with ThreadPoolExecutor(max_workers=1) as exe:
|
with ThreadPoolExecutor(max_workers=1) as exe:
|
||||||
fut = exe.submit(wda.USBClient, udid, 8100)
|
fut = exe.submit(wda.USBClient, udid, 8100)
|
||||||
return fut.result(timeout=timeout)
|
try:
|
||||||
|
client = fut.result(timeout=timeout)
|
||||||
|
LogManager.info("[CONNECT FLOW] USBClient 创建成功", udid)
|
||||||
|
return client
|
||||||
|
except Exception as e:
|
||||||
|
LogManager.error(f"[CONNECT FLOW] USBClient 创建失败(超时或异常): {e}", udid)
|
||||||
|
return None
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# 线程池里真正干活的地方(原 connectDevice 逻辑搬过来)
|
# 线程池里真正干活的地方(原 connectDevice 逻辑搬过来)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def _connect_device_task(self, udid: str):
|
def _connect_device_task(self, udid: str):
|
||||||
if not self.is_device_trusted(udid):
|
LogManager.info(f"[CONNECT FLOW] >>>>>>>>> 开始处理设备 {udid} >>>>>>>>>", udid)
|
||||||
LogManager.warning("设备未信任,跳过 WDA 启动", udid)
|
|
||||||
return
|
|
||||||
|
|
||||||
d = self._usb_client_with_timeout(udid, 8) # 8 秒必返
|
# 1. 信任检测
|
||||||
if d is None:
|
trusted = self.is_device_trusted(udid)
|
||||||
LogManager.info(f"设备链接超时,{udid}")
|
LogManager.info(f"[CONNECT FLOW] 信任检测结果 trusted={trusted}", udid)
|
||||||
|
if not trusted:
|
||||||
|
LogManager.warning("[CONNECT FLOW] 设备未信任,直接返回", udid)
|
||||||
return
|
return
|
||||||
|
LogManager.info("[CONNECT FLOW] 信任检测通过,准备创建 USBClient", udid)
|
||||||
|
|
||||||
|
# 2. 创建 WDA 客户端(带超时)
|
||||||
try:
|
try:
|
||||||
d = wda.USBClient(udid, 8100)
|
d = self._usb_client_with_timeout(udid, 8)
|
||||||
|
if d is None:
|
||||||
|
LogManager.error("[CONNECT FLOW] USBClient 返回 None(超时或异常),直接返回", udid)
|
||||||
|
return
|
||||||
|
LogManager.info("[CONNECT FLOW] USBClient 创建成功", udid)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LogManager.error(f"启动 WDA 失败: {e}", udid)
|
LogManager.error(f"[CONNECT FLOW] USBClient 抛异常: {e}", udid)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 3. 读取屏幕信息
|
||||||
width, height, scale = 0, 0, 1.0
|
width, height, scale = 0, 0, 1.0
|
||||||
try:
|
try:
|
||||||
size = d.window_size()
|
size = d.window_size()
|
||||||
width, height = size.width, size.height
|
width, height = size.width, size.height
|
||||||
scale = d.scale
|
scale = d.scale
|
||||||
|
LogManager.info(f"[CONNECT FLOW] 屏幕信息 width={width} height={height} scale={scale}", udid)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LogManager.warning(f"读取屏幕信息失败:{e}", udid)
|
LogManager.warning(f"[CONNECT FLOW] 读取屏幕信息失败: {e},继续使用默认值", udid)
|
||||||
|
|
||||||
|
# 4. 分配端口
|
||||||
port = self._alloc_port()
|
port = self._alloc_port()
|
||||||
model = DeviceModel(udid, port, width, height, scale, type=1)
|
LogManager.info(f"[CONNECT FLOW] 分配投屏端口 {port}", udid)
|
||||||
|
|
||||||
# 先做完所有 IO,再抢锁写内存
|
# 5. 启动 WDA
|
||||||
try:
|
try:
|
||||||
|
LogManager.info("[CONNECT FLOW] 正在启动 WDA 应用", udid)
|
||||||
d.app_start(WdaAppBundleId)
|
d.app_start(WdaAppBundleId)
|
||||||
d.home()
|
LogManager.info("[CONNECT FLOW] WDA 应用启动完成,等待 2s", udid)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LogManager.warning(f"启动/切回桌面失败:{e}", udid)
|
LogManager.warning(f"[CONNECT FLOW] 启动 WDA 失败: {e},仍继续", udid)
|
||||||
|
time.sleep(2)
|
||||||
time.sleep(2) # 原逻辑保留
|
|
||||||
|
|
||||||
|
# 6. 启动 iproxy
|
||||||
|
LogManager.info(f"[CONNECT FLOW] 准备启动 iproxy 本地 {port} -> 设备 9100", udid)
|
||||||
target = self.relayDeviceScreenPort(udid, port)
|
target = self.relayDeviceScreenPort(udid, port)
|
||||||
|
if target is None:
|
||||||
|
LogManager.error("[CONNECT FLOW] iproxy 启动失败,释放端口并返回", udid)
|
||||||
|
self._free_port(port)
|
||||||
|
return
|
||||||
|
LogManager.info("[CONNECT FLOW] iproxy 启动成功,进程已注册", udid)
|
||||||
|
|
||||||
# 毫秒级临界区
|
# 7. 抢锁写内存
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if udid in self._model_index: # 并发防重
|
if udid in self._model_index:
|
||||||
|
LogManager.warning(f"[CONNECT FLOW] 并发重复,已存在内存中,放弃本次任务", udid)
|
||||||
|
self._terminate_proc(target) # 避免孤儿
|
||||||
|
self._free_port(port)
|
||||||
return
|
return
|
||||||
|
model = DeviceModel(udid, port, width, height, scale, type=1)
|
||||||
self._add_model(model)
|
self._add_model(model)
|
||||||
if target:
|
self.pidList.append({"target": target, "id": udid})
|
||||||
self.pidList.append({"target": target, "id": udid})
|
LogManager.info(f"[CONNECT FLOW] 设备模型已加入内存,当前在线数: {len(self.deviceModelList)}", udid)
|
||||||
|
|
||||||
|
LogManager.info(f"[CONNECT FLOW] <<<<<<<<< 设备 {udid} 处理完成 <<<<<<<<", udid)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# 原函数保留(改名即可)
|
# 原函数保留(改名即可)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -380,31 +407,41 @@ class Deviceinfo(object):
|
|||||||
# -------------------- 工具方法(未改动) --------------------
|
# -------------------- 工具方法(未改动) --------------------
|
||||||
def is_device_trusted(self, udid: str) -> bool:
|
def is_device_trusted(self, udid: str) -> bool:
|
||||||
try:
|
try:
|
||||||
|
LogManager.info(f"[CONNECT FLOW] 开始信任检测", udid)
|
||||||
d = BaseDevice(udid)
|
d = BaseDevice(udid)
|
||||||
d.get_value("DeviceName")
|
name = d.get_value("DeviceName")
|
||||||
|
LogManager.info(f"[CONNECT FLOW] 信任检测成功,DeviceName={name}", udid)
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
LogManager.warning(f"[CONNECT FLOW] 信任检测失败: {e}", udid)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def relayDeviceScreenPort(self, udid: str, port: int) -> Optional[subprocess.Popen]:
|
def relayDeviceScreenPort(self, udid: str, port: int) -> Optional[subprocess.Popen]:
|
||||||
|
LogManager.info(f"[CONNECT FLOW] 进入 relayDeviceScreenPort,目标端口 {port}", udid)
|
||||||
if not self._spawn_iproxy:
|
if not self._spawn_iproxy:
|
||||||
LogManager.error("iproxy 启动器未就绪", udid)
|
LogManager.error("[CONNECT FLOW] _spawn_iproxy 未初始化,返回 None", udid)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
for attempt in range(5):
|
for attempt in range(5):
|
||||||
if not self._is_port_open(port):
|
if not self._is_port_open(port):
|
||||||
|
LogManager.info(f"[CONNECT FLOW] 端口 {port} 检测为空闲", udid)
|
||||||
break
|
break
|
||||||
LogManager.warning(f"端口 {port} 仍被占用,第 {attempt+1} 次重试释放", udid)
|
LogManager.warning(f"[CONNECT FLOW] 端口 {port} 仍被占用,第 {attempt + 1}/5 次尝试释放", udid)
|
||||||
pid = self._get_pid_by_port(port)
|
pid = self._get_pid_by_port(port)
|
||||||
if pid and pid != os.getpid():
|
if pid and pid != os.getpid():
|
||||||
|
LogManager.info(f"[CONNECT FLOW] 准备 kill 占用端口 {port} 的 PID {pid}", udid)
|
||||||
self._kill_pid_gracefully(pid)
|
self._kill_pid_gracefully(pid)
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
|
else:
|
||||||
|
LogManager.error("[CONNECT FLOW] 连续 5 次无法释放端口,放弃", udid)
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
p = self._spawn_iproxy(udid, port, 9100)
|
p = self._spawn_iproxy(udid, port, 9100)
|
||||||
self._port_in_use.add(port)
|
LogManager.info(f"[CONNECT FLOW] iproxy 启动完成,PID={p.pid}", udid)
|
||||||
LogManager.info(f"启动 iproxy 成功,本地 {port} -> 设备 9100", udid)
|
|
||||||
return p
|
return p
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LogManager.error(f"启动 iproxy 失败:{e}", udid)
|
LogManager.error(f"[CONNECT FLOW] iproxy 启动异常: {e}", udid)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _is_port_open(self, port: int) -> bool:
|
def _is_port_open(self, port: int) -> bool:
|
||||||
|
|||||||
Binary file not shown.
@@ -90,16 +90,23 @@ class ThreadManager:
|
|||||||
|
|
||||||
# ---------- 2. 并发等 0.2 s 收尾 ----------
|
# ---------- 2. 并发等 0.2 s 收尾 ----------
|
||||||
def _wait_and_clean(udid: str) -> Tuple[int, str]:
|
def _wait_and_clean(udid: str) -> Tuple[int, str]:
|
||||||
"""子线程里只做极短 join 并清理记录"""
|
|
||||||
with cls._lock:
|
with cls._lock:
|
||||||
task = cls._tasks.get(udid)
|
task = cls._tasks.get(udid)
|
||||||
if not task:
|
if not task:
|
||||||
return 400, f"设备{udid}任务记录已丢失"
|
return 400, "任务记录已丢失"
|
||||||
thread = task["thread"]
|
thread = task["thread"]
|
||||||
# 浅等 0.2 s,后台还没死也继续
|
# 第一次等 3 秒,让“分片睡眠”有机会退出
|
||||||
thread.join(0.2)
|
thread.join(timeout=3)
|
||||||
del cls._tasks[udid] # 立即清理
|
# 如果还活,再补 2 秒
|
||||||
return 200, f"设备{udid}已下发停止指令"
|
if thread.is_alive():
|
||||||
|
thread.join(timeout=2)
|
||||||
|
# 最终仍活,记录日志但不硬杀,避免僵尸
|
||||||
|
with cls._lock:
|
||||||
|
cls._tasks.pop(udid, None)
|
||||||
|
if thread.is_alive():
|
||||||
|
LogManager.warning(f"[batch_stop] 线程 5s 未退出,已清理记录但线程仍跑 {udid}")
|
||||||
|
return 201, "已下发停止,线程超长任务未立即结束"
|
||||||
|
return 200, "已停止"
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=min(32, len(udids))) as executor:
|
with ThreadPoolExecutor(max_workers=min(32, len(udids))) as executor:
|
||||||
future_map = {executor.submit(_wait_and_clean, udid): udid for udid in udids}
|
future_map = {executor.submit(_wait_and_clean, udid): udid for udid in udids}
|
||||||
|
|||||||
Binary file not shown.
@@ -238,7 +238,9 @@ class ScriptManager():
|
|||||||
session.double_tap(x, y)
|
session.double_tap(x, y)
|
||||||
|
|
||||||
print("--------------------------------------------")
|
print("--------------------------------------------")
|
||||||
event.wait(timeout=random.randint(300, 600))
|
# 换成
|
||||||
|
if not self.interruptible_sleep(event, random.randint(300, 600)):
|
||||||
|
break
|
||||||
session.swipe_up()
|
session.swipe_up()
|
||||||
|
|
||||||
# 正常退出(外部 event 触发)
|
# 正常退出(外部 event 触发)
|
||||||
@@ -334,8 +336,8 @@ class ScriptManager():
|
|||||||
|
|
||||||
if not anchor:
|
if not anchor:
|
||||||
LogManager.method_info(f"数据库中的数据不足", "关注打招呼", udid)
|
LogManager.method_info(f"数据库中的数据不足", "关注打招呼", udid)
|
||||||
event.wait(timeout=30)
|
if not self.interruptible_sleep(event, 30):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
aid = anchor.get("anchorId", "")
|
aid = anchor.get("anchorId", "")
|
||||||
anchorCountry = anchor.get("country", "")
|
anchorCountry = anchor.get("country", "")
|
||||||
@@ -554,7 +556,7 @@ class ScriptManager():
|
|||||||
print("----------------------------------------------------------")
|
print("----------------------------------------------------------")
|
||||||
print("监控回复消息")
|
print("监控回复消息")
|
||||||
# 执行回复消息逻辑
|
# 执行回复消息逻辑
|
||||||
self.monitorMessages(session, udid)
|
self.monitorMessages(session, udid, event)
|
||||||
|
|
||||||
homeButton = AiUtils.findHomeButton(udid)
|
homeButton = AiUtils.findHomeButton(udid)
|
||||||
if homeButton.exists:
|
if homeButton.exists:
|
||||||
@@ -901,3 +903,14 @@ class ScriptManager():
|
|||||||
else:
|
else:
|
||||||
LogManager.method_error(f"检测不到收件箱", "检测消息", udid)
|
LogManager.method_error(f"检测不到收件箱", "检测消息", udid)
|
||||||
raise Exception("当前页面找不到收件箱,重启")
|
raise Exception("当前页面找不到收件箱,重启")
|
||||||
|
|
||||||
|
|
||||||
|
# 放在 ScriptManager 类外面或 utils 里
|
||||||
|
def interruptible_sleep(self, event: threading.Event, seconds: float, slice_: float = 1.0):
|
||||||
|
"""把一次长 sleep 拆成 1 秒一片,随时响应 event"""
|
||||||
|
left = seconds
|
||||||
|
while left > 0 and not event.is_set():
|
||||||
|
timeout = min(slice_, left)
|
||||||
|
event.wait(timeout=timeout)
|
||||||
|
left -= timeout
|
||||||
|
return not event.is_set() # 返回 True 表示正常睡完,False 被中断
|
||||||
Binary file not shown.
Reference in New Issue
Block a user