refactor: 架构优化-后端(SQLite WAL/迁移机制/健康检查/uiautodev孤儿清理/残留提示)

- SQLite 开 WAL + busy_timeout + synchronous=NORMAL,避免多 worker 并发写锁
- 新增版本化 schema 迁移机制(SCHEMA_MIGRATIONS + app_meta 表记录版本)
- 新增 /api/health 健康检查(无需登录,返回设备/任务/错误摘要)
- uiautodev 用 PID 文件追踪,启动时清理上次异常退出残留的孤儿进程
- 启动时提示残留的 STF 占用(不自动释放,避免多实例误伤)
This commit is contained in:
2026-08-08 20:55:53 +08:00
parent 1fc0deaecf
commit b19caf3d55
3 changed files with 125 additions and 0 deletions
+74
View File
@@ -89,6 +89,30 @@ def logout():
return redirect(url_for("login"))
# ================== API:健康检查(无需登录,供探活) ==================
@app.route("/api/health")
def api_health():
"""轻量健康检查:返回进程/设备/任务摘要,不暴露敏感信息,供运维探活。"""
try:
devices, err = mgr.get_status()
devs = devices or []
return jsonify({
"ok": True,
"status": "up",
"time": time.time(),
"device_total": len(devs),
"device_online": sum(1 for d in devs if d.get("present")),
"device_running": sum(1 for d in devs
if d.get("worker_status") in ("running", "connecting")),
"device_error": sum(1 for d in devs
if d.get("worker_status") in ("error", "failed")),
"occupied": sum(1 for d in devs if d.get("stf_occupied")),
"jobs": len(mgr.jobs),
})
except Exception as e:
return jsonify({"ok": False, "status": "down", "error": str(e)}), 500
# ================== API:状态(监控大屏用)==================
@app.route("/api/status")
@login_required
@@ -550,6 +574,35 @@ def api_apks_install_status():
# ================== uiautodev 自动启动 ==================
_uiauto_proc = None
# PID 文件:web_server 异常退出(kill -9/崩溃)时 uiautodev 成孤儿残留,
# 用 PID 文件在下次启动时识别并清理,避免多次重启后堆积进程。
_UIAUTO_PID_FILE = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "data", "uiauto.pid")
def _kill_stale_uiauto():
"""清理上次 web_server 残留的 uiautodev 进程(读取 PID 文件)。"""
if not os.path.exists(_UIAUTO_PID_FILE):
return
try:
with open(_UIAUTO_PID_FILE) as f:
pid = int(f.read().strip())
if pid > 0 and pid != os.getpid():
try:
os.kill(pid, 0) # 探测进程是否存活
os.kill(pid, 15) # 终止残留
_log.warning(f"已清理残留的 uiautodev 进程 (PID={pid})")
except ProcessLookupError:
pass
except PermissionError:
_log.warning(f"无权限清理残留 uiautodev (PID={pid}),忽略")
except Exception:
pass
finally:
try:
os.remove(_UIAUTO_PID_FILE)
except OSError:
pass
def _ensure_uiauto_running():
@@ -559,6 +612,7 @@ def _ensure_uiauto_running():
启动失败只记日志,不影响主服务。
"""
global _uiauto_proc
_kill_stale_uiauto()
if uiauto_helper.is_running():
_log.info("uiauto2 服务已在运行 (端口 20242)")
return
@@ -572,6 +626,11 @@ def _ensure_uiauto_running():
**kwargs,
)
_log.info(f"uiautodev 服务已启动 (PID={_uiauto_proc.pid})")
try:
with open(_UIAUTO_PID_FILE, "w") as f:
f.write(str(_uiauto_proc.pid))
except OSError:
pass
atexit.register(_stop_uiauto)
except FileNotFoundError:
_log.warning("uiautodev 未安装,元素抓取功能不可用。请运行: pip install uiautodev")
@@ -582,6 +641,11 @@ def _ensure_uiauto_running():
def _stop_uiauto():
"""停止自动启动的 uiautodev 子进程。"""
global _uiauto_proc
try:
if os.path.exists(_UIAUTO_PID_FILE):
os.remove(_UIAUTO_PID_FILE)
except OSError:
pass
if not _uiauto_proc:
return
try:
@@ -634,6 +698,16 @@ if __name__ == "__main__":
from config import WEB_HOST, WEB_PORT
# 自动启动 uiautodev 服务(元素抓取功能依赖,端口 20242)
_ensure_uiauto_running()
# 启动时提示可能残留的 STF 占用(上次异常退出/强杀遗留)。
# 不自动释放:本机与生产可能是多实例共用一个 STF 账户,误放会中断另一实例的任务。
try:
_mine = stf.list_my_devices()
if _mine:
_serials = ", ".join(d["serial"] for d in _mine)
_log.warning(f"启动时发现 {len(_mine)} 台设备仍被本账户占用(可能异常退出残留):{_serials}。"
f"确认无其他实例在跑时可用\"强制释放占用\"清理")
except Exception:
pass
_log.info(f"管理后台: http://localhost:{WEB_PORT}/ (admin/admin123)")
try:
_run_server(WEB_HOST, WEB_PORT)