feat: 维护 Tab(STF 容器一键重启 + adb 远程终端,含设备快捷选择/卸载 agent/重连)

This commit is contained in:
2026-08-11 08:27:33 +08:00
parent 26c908f04c
commit 3320035c9e
4 changed files with 276 additions and 6 deletions
+120
View File
@@ -18,6 +18,8 @@ import time
import uuid
import atexit
import secrets
import functools
import shlex
import subprocess
from datetime import datetime
@@ -732,6 +734,124 @@ def api_apks_install_status():
return jsonify({"ok": True, "status": status})
# ================== API:维护(仅管理员) ==================
# 维护功能:STF 容器一键重启 + adb 远程终端。两个接口都仅管理员可用。
# adb 终端安全红线(技术约束):绝不 kill-server / 绝不 disconnect IP:5555——
# STF provider 共享该 adb transport,断开会让 STF 误判全部设备离线并触发重连。
_ADB_BLOCKED_PATTERNS = ("kill-server", "disconnect")
def _blocked_adb_cmd(cmd):
"""命中红线的 adb 命令(kill-server / disconnect)直接拒绝。"""
low = cmd.lower()
return any(p in low for p in _ADB_BLOCKED_PATTERNS)
@app.route("/api/adb/devices")
@admin_required
def api_adb_devices():
"""维护终端设备列表(仅管理员):本地 adb 已连接 + STF 在线设备池。
供终端设备选择器使用——选中后自动附加 `-s <serial>`,
离线/未连接的设备也可选,配合"重连设备"按钮恢复。
"""
states = {}
out = _adb("devices")
for line in (out or "").splitlines()[1:]:
parts = line.split()
if len(parts) >= 2 and parts[0] and not parts[0].startswith("*"):
states[parts[0]] = parts[1]
try:
for d in stf.list_all_devices():
serial = d.get("serial", "")
if serial and d.get("present") and d.get("ready") and serial not in states:
states[serial] = "stf"
except Exception:
pass
devices = [{"serial": s, "state": st} for s, st in sorted(states.items())]
return jsonify({"ok": True, "devices": devices})
@app.route("/api/adb/cmd", methods=["POST"])
@admin_required
def api_adb_cmd():
"""维护终端:执行 adb 命令(仅管理员)。
请求: {"cmd": "adb -s 100.100.10.11:5555 shell ls /sdcard"}
用平台 adb 二进制执行(config.ADB_PATH),带 20s 超时。
"""
data = request.json or {}
cmd = (data.get("cmd") or "").strip()
if not cmd:
return jsonify({"ok": False, "error": "命令不能为空"}), 400
if _blocked_adb_cmd(cmd):
return jsonify({"ok": False,
"error": "禁止执行 kill-server / disconnect(会中断 STF 设备监控,影响所有任务)"}), 400
tokens = shlex.split(cmd)
if tokens and tokens[0] == "adb":
tokens = tokens[1:]
if not tokens:
return jsonify({"ok": False, "error": "命令不能为空"}), 400
try:
# 与 worker 的 adb 调用共用锁,避免并发操作同一 adb server
with _ADB_LOCK:
r = subprocess.run([ADB_PATH, *tokens], capture_output=True, timeout=20)
out = (r.stdout or b"").decode("utf-8", errors="replace")
err = (r.stderr or b"").decode("utf-8", errors="replace")
_log.info(f"维护终端执行 adb {' '.join(tokens)} -> exit {r.returncode}")
return jsonify({"ok": True, "stdout": out, "stderr": err, "code": r.returncode})
except subprocess.TimeoutExpired:
return jsonify({"ok": False, "error": "命令执行超时(20s)"}), 408
except Exception as e:
_log.warning(f"维护终端 adb 执行异常: {e}")
return jsonify({"ok": False, "error": f"执行失败: {e}"}), 500
@app.route("/api/stf/restart", methods=["POST"])
@admin_required
def api_stf_restart():
"""一键重启 STF Docker 容器(仅管理员)。
通过 SSH(config.STF_SSH_TARGET,默认 [email protected])执行
`docker restart <容器>`(config.STF_DOCKER_CONTAINER,默认 stf),
随后轮询 STF API 确认恢复。
影响:STF 约 30s 不可用,期间占用状态重置、任务中断——有任务在跑时拒绝执行。
"""
running = mgr.get_running()
if running:
return jsonify({"ok": False,
"error": f"有 {len(running)} 台设备正在运行任务,请先停止再维护"}), 409
ssh_cmd = ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5",
"-o", "StrictHostKeyChecking=no", STF_SSH_TARGET,
f"docker restart {STF_DOCKER_CONTAINER} && "
f"docker ps --filter name={STF_DOCKER_CONTAINER} --format '{{{{.Names}}}}: {{{{.Status}}}}'"]
try:
r = subprocess.run(ssh_cmd, capture_output=True, timeout=60)
except subprocess.TimeoutExpired:
return jsonify({"ok": False, "error": "SSH 执行超时(60s)"}), 408
except FileNotFoundError:
return jsonify({"ok": False, "error": "本机未安装 ssh,无法远程操作"}), 500
out = (r.stdout or b"").decode("utf-8", errors="replace")
err = (r.stderr or b"").decode("utf-8", errors="replace")
if r.returncode != 0:
_log.error(f"STF 容器重启失败: {err or out}")
return jsonify({"ok": False,
"error": f"SSH 执行失败:{err or out}(请确认本机可免密 SSH 到 {STF_SSH_TARGET},且容器名 {STF_DOCKER_CONTAINER} 正确)"}), 502
# 轮询 STF API 确认恢复(最长 30s)
alive = False
for _ in range(30):
try:
stf.list_all_devices()
alive = True
break
except Exception:
time.sleep(1)
_log.info(f"STF 容器 {STF_DOCKER_CONTAINER} 已重启,API 恢复={alive}")
return jsonify({"ok": True,
"msg": "STF 容器已重启" + ("" if alive else "(STF API 尚未恢复,请稍后刷新)"),
"detail": out.strip(), "api_alive": alive})
# ================== uiautodev 自动启动 ==================
_uiauto_proc = None
# PID 文件:web_server 异常退出(kill -9/崩溃)时 uiautodev 成孤儿残留,