chore: 初始化仓库(排除 adb 二进制文件以解决推送超时)
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
"""adb 命令工具封装。
|
||||
|
||||
并发关键:adb_connect 绝不 kill-server(会断开其他 worker 正在用的设备),
|
||||
用全局锁串行化连接阶段,connect 本身很快,串行不影响整体并发。
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
from config import ADB_PATH
|
||||
from core.logger import get_logger
|
||||
|
||||
_log = get_logger("core.adb")
|
||||
|
||||
_ADB_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def _adb(*args):
|
||||
"""执行 adb 命令,返回 (stdout+stderr) 文本。不用 shell=True,避免子进程环境问题。
|
||||
|
||||
用 bytes 模式自己解码(Windows 下 subprocess text=True 会用 GBK 读 adb 输出,
|
||||
遇到非 GBK 字节会崩溃)。
|
||||
"""
|
||||
r = subprocess.run([ADB_PATH, *args], capture_output=True)
|
||||
out = (r.stdout or b"") + (r.stderr or b"")
|
||||
try:
|
||||
return out.decode("utf-8", errors="replace").strip()
|
||||
except Exception:
|
||||
return out.decode("gbk", errors="replace").strip()
|
||||
|
||||
|
||||
def get_foreground_app(remote_adb_url, timeout=3):
|
||||
"""获取设备当前前台 App 包名。
|
||||
|
||||
通过 adb shell dumpsys window 解析 mCurrentFocus。
|
||||
remote_adb_url: adb 连接地址(如 192.168.20.17:12345),设备需已 adb connect。
|
||||
返回包名字符串(如 com.ss.android.ugc.aweme),失败/无设备返回 None。
|
||||
"""
|
||||
if not remote_adb_url:
|
||||
return None
|
||||
try:
|
||||
r = subprocess.run(
|
||||
[ADB_PATH, "-s", remote_adb_url, "shell",
|
||||
"dumpsys window | grep mCurrentFocus"],
|
||||
capture_output=True, timeout=timeout
|
||||
)
|
||||
out = (r.stdout or b"").decode("utf-8", errors="replace").strip()
|
||||
if not out:
|
||||
return None
|
||||
low = out.lower()
|
||||
if "device offline" in low or "device not found" in low or "error:" in low:
|
||||
return None
|
||||
# 解析 mCurrentFocus=Window{abc u0 com.xx/com.xx.MainActivity}
|
||||
for line in out.split("\n"):
|
||||
if "mCurrentFocus" not in line:
|
||||
continue
|
||||
# 提取 u0 后面的 token(包名/Activity 或包名)
|
||||
m = re.search(r'\bu0\s+(\S+)', line)
|
||||
if m:
|
||||
token = m.group(1).rstrip("}")
|
||||
if "/" in token:
|
||||
return token.split("/")[0]
|
||||
return token or None
|
||||
return None
|
||||
except subprocess.TimeoutExpired:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def adb_connect(remote_adb_url, retries=5):
|
||||
"""adb connect(带重试)。返回 True/False。
|
||||
|
||||
首次常因认证握手未就绪报 failed to authenticate,第2次重试通常 already connected。
|
||||
STF remoteConnect 启用后端口监听可能需要几秒,所以重试间隔递增。
|
||||
若全部重试失败,做一次 kill-server 硬重置后再试(long-running 进程的 adb daemon
|
||||
可能状态坏掉,必须重置)。硬重置在全局锁内,会短暂影响并发 worker,但只在万不得已时触发。
|
||||
"""
|
||||
with _ADB_LOCK:
|
||||
if _try_connect(remote_adb_url, retries):
|
||||
return True
|
||||
# 全部失败,硬重置 adb server
|
||||
_log.warning(f"{remote_adb_url} 重试耗尽,执行 kill-server 硬重置")
|
||||
_adb("kill-server")
|
||||
time.sleep(2)
|
||||
_adb("start-server")
|
||||
time.sleep(2)
|
||||
return _try_connect(remote_adb_url, 3)
|
||||
return False
|
||||
|
||||
|
||||
def _try_connect(remote_adb_url, retries):
|
||||
"""尝试连接,不 kill-server。返回 True/False。"""
|
||||
for attempt in range(1, retries + 1):
|
||||
out = _adb("connect", remote_adb_url)
|
||||
_log.info(f"{remote_adb_url} 第 {attempt} 次尝试: {out}")
|
||||
if "connected" in out.lower() and "failed" not in out.lower():
|
||||
return True
|
||||
time.sleep(1 + attempt)
|
||||
return False
|
||||
|
||||
|
||||
def adb_connect_light(remote_adb_url):
|
||||
"""轻量 adb connect(扫描专用):单次尝试,不重试,绝不 kill-server。
|
||||
|
||||
与 adb_connect 的区别:
|
||||
- 不重试:失败立即返回 False
|
||||
- 不 kill-server:避免断开其他 worker 正在使用的 ADB 连接
|
||||
用于前台 App 扫描等非关键路径,连不上就跳过,不影响正在运行的任务。
|
||||
"""
|
||||
with _ADB_LOCK:
|
||||
out = _adb("connect", remote_adb_url)
|
||||
if "connected" in out.lower() and "failed" not in out.lower():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def adb_disconnect(remote_adb_url):
|
||||
"""adb disconnect。"""
|
||||
try:
|
||||
_adb("disconnect", remote_adb_url)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def identify_device(serial, duration=3):
|
||||
"""让设备响铃以供识别(直连 serial,不经过 STF)。
|
||||
|
||||
播放铃声 duration 秒后自动停止。
|
||||
不 disconnect(避免断开 STF agent 共享的 adb 连接)。
|
||||
返回 (ok, msg)。
|
||||
"""
|
||||
import subprocess
|
||||
if not adb_connect_light(serial):
|
||||
return False, "adb connect 失败"
|
||||
try:
|
||||
# 异步启动铃声播放(cmd audio play-ringtone 会阻塞,用 Popen)
|
||||
p = subprocess.Popen(
|
||||
[ADB_PATH, "-s", serial, "shell", "cmd", "audio", "play-ringtone"],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
)
|
||||
time.sleep(duration)
|
||||
p.terminate()
|
||||
# 停止铃声
|
||||
subprocess.run(
|
||||
[ADB_PATH, "-s", serial, "shell", "cmd", "audio", "stop-ringtone"],
|
||||
capture_output=True, timeout=5,
|
||||
)
|
||||
_log.info(f"[{serial}] 识别响铃 {duration}s 完成")
|
||||
return True, f"已响铃 {duration} 秒"
|
||||
except Exception as e:
|
||||
_log.warning(f"[{serial}] 识别响铃失败: {e}")
|
||||
return False, str(e)
|
||||
Reference in New Issue
Block a user