feat: 维护 Tab(STF 容器一键重启 + adb 远程终端,含设备快捷选择/卸载 agent/重连)
This commit is contained in:
+120
@@ -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 成孤儿残留,
|
||||
|
||||
Reference in New Issue
Block a user