feat: STF设备管理彻底删除(脚本+断开+STF API清记录)+ 一键重连(后台跑connect脚本)+ 跳过原因日志区分离线/未就绪 + SSH超时错误分类
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
失败抛 SSHError(连接失败/超时),由调用方转成友好错误返回。
|
失败抛 SSHError(连接失败/超时),由调用方转成友好错误返回。
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import paramiko
|
import paramiko
|
||||||
@@ -91,6 +92,8 @@ def _run_paramiko(cmd, timeout):
|
|||||||
client.close()
|
client.close()
|
||||||
except paramiko.AuthenticationException:
|
except paramiko.AuthenticationException:
|
||||||
raise SSHError(f"SSH 密码认证失败(请检查 .env 的 STF_SSH_PASSWORD)")
|
raise SSHError(f"SSH 密码认证失败(请检查 .env 的 STF_SSH_PASSWORD)")
|
||||||
|
except socket.timeout:
|
||||||
|
raise SSHError(f"SSH 执行超时({timeout}s 内未完成,命令可能耗时较长)")
|
||||||
except paramiko.SSHException as e:
|
except paramiko.SSHException as e:
|
||||||
raise SSHError(f"SSH 连接失败: {e}")
|
raise SSHError(f"SSH 连接失败: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -160,6 +160,25 @@ class STFClient:
|
|||||||
_log.error(f"释放 {serial} 失败: {msg}")
|
_log.error(f"释放 {serial} 失败: {msg}")
|
||||||
return released, failed
|
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 隧道 ==================
|
# ================== 远程 ADB 隧道 ==================
|
||||||
def remote_connect(self, serial):
|
def remote_connect(self, serial):
|
||||||
"""建立远程 ADB 隧道,返回 remoteConnectUrl。
|
"""建立远程 ADB 隧道,返回 remoteConnectUrl。
|
||||||
|
|||||||
@@ -131,3 +131,28 @@ def remove_device(ip):
|
|||||||
msgs.append(out.strip() or (f"adb disconnect 执行完成(exit {code})" if code == 0 else "adb disconnect 无输出"))
|
msgs.append(out.strip() or (f"adb disconnect 执行完成(exit {code})" if code == 0 else "adb disconnect 无输出"))
|
||||||
_log.info(f"STF 移除设备 {ip}: {msgs}")
|
_log.info(f"STF 移除设备 {ip}: {msgs}")
|
||||||
return 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
@@ -156,19 +156,29 @@ class TaskJob:
|
|||||||
return [d["serial"] for d in manager.stf.list_free_devices()]
|
return [d["serial"] for d in manager.stf.list_free_devices()]
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
# 离线自动跳过(serial/group 模式):跳过 STF 池里不在线(present=False)的设备,
|
# 离线自动跳过(serial/group 模式):跳过 STF 池里不在线(present=False)或
|
||||||
# 避免对离线设备反复尝试占用后报"设备离线"。默认开启,可在任务编辑器取消勾选。
|
# 未就绪(ready=False,provider 刚接入还在初始化)的设备,
|
||||||
|
# 避免对离线/未就绪设备反复尝试占用后报"设备离线"。默认开启,可在任务编辑器取消勾选。
|
||||||
if serials and self.params.get("skip_offline", True):
|
if serials and self.params.get("skip_offline", True):
|
||||||
try:
|
try:
|
||||||
online = {d["serial"] for d in manager.stf.list_all_devices()
|
devs = manager.stf.list_all_devices()
|
||||||
if d.get("present") and d.get("ready")}
|
|
||||||
except Exception:
|
except Exception:
|
||||||
online = None # STF 查询失败时不过滤,维持原行为
|
devs = None # STF 查询失败时不过滤,维持原行为
|
||||||
if online is not None:
|
if devs is not None:
|
||||||
skipped = [s for s in serials if s not in online]
|
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:
|
if skipped:
|
||||||
_log.info(f"任务 {self.name} 离线自动跳过 {len(skipped)} 台设备: {skipped}")
|
detail = ", ".join(f"{s}({why})" for s, why in skipped)
|
||||||
serials = [s for s in serials if s in online]
|
_log.info(f"任务 {self.name} 跳过 {len(skipped)} 台设备: {detail}")
|
||||||
|
serials = kept
|
||||||
return serials
|
return serials
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+31
-1
@@ -126,7 +126,37 @@ function renderStfDevRow(x){
|
|||||||
return '<tr><td style="font-family:monospace">'+esc(x.ip)+'</td>'+
|
return '<tr><td style="font-family:monospace">'+esc(x.ip)+'</td>'+
|
||||||
'<td>'+(x.inCfg?'<span class="label label-info">脚本中</span>':'<span class="label label-default">未配置</span>')+'</td>'+
|
'<td>'+(x.inCfg?'<span class="label label-info">脚本中</span>':'<span class="label label-default">未配置</span>')+'</td>'+
|
||||||
'<td>'+stHtml+'</td>'+
|
'<td>'+stHtml+'</td>'+
|
||||||
'<td><button class="btn btn-xs btn-danger" onclick="removeStfDev(\''+esc(x.ip)+'\')">移除(断开+删脚本)</button></td></tr>';
|
'<td style="white-space:nowrap">'+
|
||||||
|
'<button class="btn btn-xs btn-danger" onclick="removeStfDev(\''+esc(x.ip)+'\')" title="断开+删脚本">移除</button> '+
|
||||||
|
'<button class="btn btn-xs btn-danger" onclick="deleteStfDev(\''+esc(x.ip)+'\')" title="断开+删脚本+清 STF 池记录(设备从池中彻底消失)">彻底删除</button>'+
|
||||||
|
'</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 一键重连:手动运行 220 的 connect_devices.sh,补连所有配置设备
|
||||||
|
async function reconnectStfDevices(){
|
||||||
|
if(!confirm('立即运行 220 上的 connect_devices.sh?\n\n会补连脚本里所有未连接的设备(在线设备跳过)。'))return;
|
||||||
|
const r=await apiPost('/api/stf/devmgmt/reconnect',{});
|
||||||
|
if(!r)return;
|
||||||
|
const box=document.getElementById('stfdev-reconnect-out');
|
||||||
|
if(r.ok){
|
||||||
|
box.style.display='block';
|
||||||
|
box.textContent=r.output||'执行完成';
|
||||||
|
showToast('重连完成','success');
|
||||||
|
loadStfDevMgmt();
|
||||||
|
}else{
|
||||||
|
showToast('重连失败: '+(r.error||''),'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 彻底删除:脚本移除 + 断开 + 清 STF 池记录(设备从池中彻底消失)
|
||||||
|
async function deleteStfDev(ip){
|
||||||
|
if(!confirm('彻底删除 '+ip+'?\n\n1. 从 connect_devices.sh 移除\n2. 断开 220 的 adb 连接\n3. 调用 STF API 清除设备记录(设备从设备池彻底消失,需重新添加才会回来)'))return;
|
||||||
|
const r=await apiPost('/api/stf/devmgmt/delete',{ip:ip});
|
||||||
|
if(!r)return;
|
||||||
|
if(r.ok){
|
||||||
|
showToast((r.msgs||[]).join(';'),'success');
|
||||||
|
loadStfDevMgmt();
|
||||||
|
}else showToast('删除失败: '+(r.error||''),'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addStfDev(){
|
async function addStfDev(){
|
||||||
|
|||||||
@@ -642,10 +642,12 @@ select.form-control{cursor:pointer}
|
|||||||
<input type="text" id="stfdev-ip" class="form-control" style="width:220px;font-family:monospace"
|
<input type="text" id="stfdev-ip" class="form-control" style="width:220px;font-family:monospace"
|
||||||
placeholder="设备 IP,如 100.100.10.20" onkeydown="if(event.key==='Enter')addStfDev()">
|
placeholder="设备 IP,如 100.100.10.20" onkeydown="if(event.key==='Enter')addStfDev()">
|
||||||
<button class="btn btn-primary" onclick="addStfDev()">添加设备并连接</button>
|
<button class="btn btn-primary" onclick="addStfDev()">添加设备并连接</button>
|
||||||
|
<button class="btn" onclick="reconnectStfDevices()" title="立即运行 220 的 connect_devices.sh,补连所有配置设备">一键重连</button>
|
||||||
<button class="btn" onclick="loadStfDevMgmt()">刷新</button>
|
<button class="btn" onclick="loadStfDevMgmt()">刷新</button>
|
||||||
<input class="form-control list-search" id="search-stfdev" placeholder="搜索 IP...">
|
<input class="form-control list-search" id="search-stfdev" placeholder="搜索 IP...">
|
||||||
<span id="stfdev-status" class="text-muted"></span>
|
<span id="stfdev-status" class="text-muted"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="help" id="stfdev-reconnect-out" style="display:none;background:#0f172a;color:#e2e8f0;font-family:monospace;font-size:12px;padding:10px;border-radius:8px;margin-bottom:10px;white-space:pre-wrap"></div>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th data-sort="ip" onclick="_lpSort('stfdev','ip')">设备</th>
|
<th data-sort="ip" onclick="_lpSort('stfdev','ip')">设备</th>
|
||||||
|
|||||||
@@ -1278,6 +1278,31 @@ def api_tools_appver():
|
|||||||
"total": len(results)})
|
"total": len(results)})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/stf/devmgmt/delete", methods=["POST"])
|
||||||
|
@admin_required
|
||||||
|
def api_stf_devmgmt_delete():
|
||||||
|
"""STF 设备管理:彻底删除(脚本移除 + disconnect + 清 STF 池记录)。"""
|
||||||
|
ip = (request.json or {}).get("ip", "").strip()
|
||||||
|
if not ip:
|
||||||
|
return jsonify({"ok": False, "error": "请输入设备 IP"}), 400
|
||||||
|
try:
|
||||||
|
msgs = stf_device_mgmt.delete_device(ip, stf=stf)
|
||||||
|
return jsonify({"ok": True, "msgs": msgs})
|
||||||
|
except StfDevError as e:
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 502
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/stf/devmgmt/reconnect", methods=["POST"])
|
||||||
|
@admin_required
|
||||||
|
def api_stf_devmgmt_reconnect():
|
||||||
|
"""STF 设备管理:一键重连(手动运行 220 的 connect_devices.sh)。"""
|
||||||
|
try:
|
||||||
|
ok, summary = stf_device_mgmt.reconnect_all()
|
||||||
|
return jsonify({"ok": True, "output": summary})
|
||||||
|
except StfDevError as e:
|
||||||
|
return jsonify({"ok": False, "error": str(e)}), 502
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/stf/restart", methods=["POST"])
|
@app.route("/api/stf/restart", methods=["POST"])
|
||||||
@admin_required
|
@admin_required
|
||||||
def api_stf_restart():
|
def api_stf_restart():
|
||||||
|
|||||||
Reference in New Issue
Block a user