134 lines
5.2 KiB
Python
134 lines
5.2 KiB
Python
"""OpenSTF 设备池管理(工具页"STF 设备管理"用)。
|
||
|
||
背景:220 上 STF 的 adb 跑在 Docker 容器里,设备池由
|
||
/mnt/data/openstf/connect_devices.sh 的 DEVICES 数组维护(cron 每 5 分钟补连)。
|
||
本模块把"改脚本 + 实时 adb connect/disconnect"封装成 API:
|
||
- status 已配置 IP 列表 + 220 adb 实际连接状态
|
||
- add_device 写入脚本 DEVICES + 立即 docker exec adb adb connect
|
||
- remove_device 从脚本 DEVICES 移除 + adb disconnect
|
||
|
||
脚本编辑用 220 上的 python3 做精确的行级增删(sed 处理多行数组容易误伤)。
|
||
注意:这里的 adb 是 220 上的(STF provider 的),与本机 Mac 直连的 adb 相互独立;
|
||
移除设备会断开 STF 池连接(设备从 OpenSTF 下线),不影响本机已有的任务 adb 连接。
|
||
"""
|
||
import re
|
||
import shlex
|
||
|
||
from config import STF_ADB_CONTAINER, STF_SCRIPT_PATH
|
||
from core.logger import get_logger
|
||
from core.ssh_client import run as _ssh_run, SSHError
|
||
|
||
_log = get_logger("core.stfdev")
|
||
|
||
|
||
class StfDevError(Exception):
|
||
"""STF 设备管理错误(SSH/脚本/命令失败)。"""
|
||
|
||
|
||
def _ssh(cmd, timeout=25):
|
||
"""在 220 上执行一条 shell 命令(认证见 core.ssh_client)。返回 (code, stdout, stderr)。"""
|
||
try:
|
||
return _ssh_run(cmd, timeout=timeout)
|
||
except SSHError as e:
|
||
raise StfDevError(str(e))
|
||
|
||
|
||
def _adb(args, remote_timeout=15):
|
||
"""在 220 的 adb 容器里执行 adb 命令。返回 (code, stdout, stderr)。
|
||
|
||
remote_timeout 是 220 侧的 timeout 秒数:adb connect 到不可达 IP 会长时间
|
||
挂起重试(实测 ≥40s),必须用 host 的 timeout 兜底,否则 API 请求会卡死。
|
||
"""
|
||
return _ssh(f"timeout {remote_timeout} docker exec {shlex.quote(STF_ADB_CONTAINER)} adb "
|
||
+ " ".join(shlex.quote(a) for a in args))
|
||
|
||
|
||
def _norm_ip(ip):
|
||
"""规范化 IP:去空白、去 :5555 后缀。"""
|
||
ip = str(ip or "").strip()
|
||
if ip.endswith(":5555"):
|
||
ip = ip[:-5]
|
||
return ip
|
||
|
||
|
||
# ================== 查询 ==================
|
||
def configured_ips():
|
||
"""解析脚本 DEVICES 数组里的 IP 列表。"""
|
||
code, out, err = _ssh(f"cat {shlex.quote(STF_SCRIPT_PATH)}")
|
||
if code != 0:
|
||
raise StfDevError(f"读取脚本失败: {err.strip()}")
|
||
m = re.search(r"DEVICES=\((.*?)\)", out, re.S)
|
||
if not m:
|
||
raise StfDevError("脚本中未找到 DEVICES 列表")
|
||
return re.findall(r'"([^"]+)"', m.group(1))
|
||
|
||
|
||
def connected_devices():
|
||
"""220 adb 实际连接状态 [{serial, state}]。"""
|
||
code, out, err = _adb(["devices"])
|
||
if code != 0:
|
||
raise StfDevError(f"adb devices 失败: {err.strip()}")
|
||
devs = []
|
||
for line in out.splitlines()[1:]:
|
||
parts = line.split()
|
||
if len(parts) >= 2 and parts[0] and not parts[0].startswith("*"):
|
||
devs.append({"serial": parts[0], "state": parts[1]})
|
||
return devs
|
||
|
||
|
||
def status():
|
||
"""综合状态:脚本配置的 IP + 实际连接。返回 {configured, connected}。"""
|
||
return {"configured": configured_ips(), "connected": connected_devices()}
|
||
|
||
|
||
# ================== 增删设备 ==================
|
||
def _edit_script(python_code):
|
||
"""用 220 的 python3 执行脚本编辑代码(代码内请用双引号)。"""
|
||
cmd = f"python3 -c {shlex.quote(python_code)}"
|
||
code, out, err = _ssh(cmd)
|
||
if code != 0:
|
||
raise StfDevError(f"脚本编辑失败: {err.strip() or out.strip()}")
|
||
|
||
|
||
def add_device(ip):
|
||
"""添加设备:写入脚本 DEVICES + 立即 connect。返回操作消息列表。"""
|
||
ip = _norm_ip(ip)
|
||
if not re.match(r"^\d{1,3}(\.\d{1,3}){3}$", ip):
|
||
raise StfDevError(f"IP 格式不正确: {ip}")
|
||
msgs = []
|
||
if ip in configured_ips():
|
||
msgs.append(f"{ip} 已在脚本配置中")
|
||
else:
|
||
# 在 DEVICES=( 行后插入一行 IP(python 精确行级插入)
|
||
_edit_script(
|
||
"p='" + STF_SCRIPT_PATH + "';s=open(p).read();"
|
||
"s=s.replace('DEVICES=(\\n','DEVICES=(\\n \\\"" + ip + "\\\"\\n',1);open(p,'w').write(s)")
|
||
msgs.append(f"已写入脚本 {STF_SCRIPT_PATH}")
|
||
code, out, err = _adb(["connect", f"{ip}:5555"])
|
||
if code == 0:
|
||
msgs.append(out.strip() or "adb connect 成功")
|
||
elif code == 124:
|
||
msgs.append("连接超时(设备当前不可达),已保留在脚本中,cron 会每 5 分钟自动重试")
|
||
else:
|
||
msgs.append(f"连接失败: {out.strip() or err.strip() or '未知错误'}")
|
||
_log.info(f"STF 添加设备 {ip}: {msgs}")
|
||
return msgs
|
||
|
||
|
||
def remove_device(ip):
|
||
"""移除设备:从脚本 DEVICES 删除 + adb disconnect。返回操作消息列表。"""
|
||
ip = _norm_ip(ip)
|
||
msgs = []
|
||
if ip in configured_ips():
|
||
# 删除 DEVICES 块内该 IP 的整行
|
||
_edit_script(
|
||
"p='" + STF_SCRIPT_PATH + "';s=open(p).read();"
|
||
"s=s.replace(' \\\"" + ip + "\\\"\\n','');open(p,'w').write(s)")
|
||
msgs.append("已从脚本移除")
|
||
else:
|
||
msgs.append(f"{ip} 不在脚本配置中")
|
||
code, out, err = _adb(["disconnect", f"{ip}:5555"])
|
||
msgs.append(out.strip() or (f"adb disconnect 执行完成(exit {code})" if code == 0 else "adb disconnect 无输出"))
|
||
_log.info(f"STF 移除设备 {ip}: {msgs}")
|
||
return msgs
|