feat: STF设备管理彻底删除(脚本+断开+STF API清记录)+ 一键重连(后台跑connect脚本)+ 跳过原因日志区分离线/未就绪 + SSH超时错误分类

This commit is contained in:
2026-08-15 15:22:55 +08:00
parent e12219c979
commit 17f849f723
7 changed files with 124 additions and 10 deletions
+3
View File
@@ -10,6 +10,7 @@
失败抛 SSHError(连接失败/超时),由调用方转成友好错误返回。
"""
import re
import socket
import subprocess
import paramiko
@@ -91,6 +92,8 @@ def _run_paramiko(cmd, timeout):
client.close()
except paramiko.AuthenticationException:
raise SSHError(f"SSH 密码认证失败(请检查 .env 的 STF_SSH_PASSWORD)")
except socket.timeout:
raise SSHError(f"SSH 执行超时({timeout}s 内未完成,命令可能耗时较长)")
except paramiko.SSHException as e:
raise SSHError(f"SSH 连接失败: {e}")
except Exception as e:
+19
View File
@@ -160,6 +160,25 @@ class STFClient:
_log.error(f"释放 {serial} 失败: {msg}")
return released, failed
def delete_device(self, serial):
"""从 STF 池彻底删除设备记录(清理离线幽灵设备用)。
DELETE /api/v1/devices/{serial}(实测可用):断开后 STF 数据库里残留的
present=False 陈旧记录会一直显示在设备池,用此端点彻底移除。
"""
import urllib.parse
try:
r = requests.delete(
f"{self.base_url}/api/v1/devices/{urllib.parse.quote(serial, safe='')}",
headers=self.headers, timeout=_TIMEOUT)
if r.status_code in (200, 202, 204):
_log.info(f"已从 STF 池删除设备 {serial}")
return True, "已从 STF 池删除"
return False, f"HTTP {r.status_code}: {r.text[:150]}"
except requests.exceptions.RequestException as e:
_log.warning(f"删除 STF 设备 {serial} 请求异常: {e}")
return False, str(e)
# ================== 远程 ADB 隧道 ==================
def remote_connect(self, serial):
"""建立远程 ADB 隧道,返回 remoteConnectUrl。
+25
View File
@@ -131,3 +131,28 @@ def remove_device(ip):
msgs.append(out.strip() or (f"adb disconnect 执行完成(exit {code})" if code == 0 else "adb disconnect 无输出"))
_log.info(f"STF 移除设备 {ip}: {msgs}")
return msgs
def delete_device(ip, stf=None):
"""彻底删除设备:脚本移除 + disconnect + 清 STF 池记录(幽灵设备)。"""
msgs = remove_device(ip)
if stf is not None:
ok, msg = stf.delete_device(f"{_norm_ip(ip)}:5555")
msgs.append(msg)
_log.info(f"STF 彻底删除设备 {ip}: {msgs}")
return msgs
def reconnect_all():
"""一键重连:后台运行 220 的 connect_devices.sh(cron 之外的立即补连)。
脚本对未连接设备逐个 connect(不可达设备可能挂 40s+),全程可能 1-3 分钟,
因此 nohup 后台执行立即返回,不阻塞请求;结果用"刷新"看设备连接状态。
"""
cmd = (f"nohup bash {shlex.quote(STF_SCRIPT_PATH)} >/tmp/connect_devices.out 2>&1 &")
code, out, err = _ssh(cmd, timeout=15)
if code != 0:
raise StfDevError(f"启动重连失败: {err.strip() or out.strip()}")
_log.info("STF 一键重连已在 220 后台启动")
return True, ("重连已在 220 后台执行(对未连接设备逐个 connect,约 1-3 分钟),"
"稍后点\"刷新\"查看设备连接状态")
+19 -9
View File
@@ -156,19 +156,29 @@ class TaskJob:
return [d["serial"] for d in manager.stf.list_free_devices()]
except Exception:
return []
# 离线自动跳过(serial/group 模式):跳过 STF 池里不在线(present=False)的设备,
# 避免对离线设备反复尝试占用后报"设备离线"。默认开启,可在任务编辑器取消勾选。
# 离线自动跳过(serial/group 模式):跳过 STF 池里不在线(present=False)或
# 未就绪(ready=False,provider 刚接入还在初始化)的设备,
# 避免对离线/未就绪设备反复尝试占用后报"设备离线"。默认开启,可在任务编辑器取消勾选。
if serials and self.params.get("skip_offline", True):
try:
online = {d["serial"] for d in manager.stf.list_all_devices()
if d.get("present") and d.get("ready")}
devs = manager.stf.list_all_devices()
except Exception:
online = None # STF 查询失败时不过滤,维持原行为
if online is not None:
skipped = [s for s in serials if s not in online]
devs = None # STF 查询失败时不过滤,维持原行为
if devs is not None:
present = {d["serial"] for d in devs if d.get("present")}
ready = {d["serial"] for d in devs if d.get("present") and d.get("ready")}
kept, skipped = [], []
for s in serials:
if s not in present:
skipped.append((s, "离线"))
elif s not in ready:
skipped.append((s, "未就绪(provider初始化中)"))
else:
kept.append(s)
if skipped:
_log.info(f"任务 {self.name} 离线自动跳过 {len(skipped)} 台设备: {skipped}")
serials = [s for s in serials if s in online]
detail = ", ".join(f"{s}({why})" for s, why in skipped)
_log.info(f"任务 {self.name} 跳过 {len(skipped)} 台设备: {detail}")
serials = kept
return serials