diff --git a/core/device_worker.py b/core/device_worker.py index e984b1d..5990141 100644 --- a/core/device_worker.py +++ b/core/device_worker.py @@ -115,6 +115,36 @@ def _remove_worker(serial): _WORKERS.pop(serial, None) +def clear_worker_error(serial): + """清除设备错误/失败状态,恢复为 idle。运行中/连接中返回 False 不清理。 + + 检查与更新在同一把锁内完成,避免与任务启动写 connecting/running 的竞态。 + """ + with _WORKERS_LOCK: + w = _WORKERS.get(serial) + if w is None: + return True # 本就没有状态记录,视作已清理 + if w.get("status") in ("running", "connecting"): + return False + w.update(status="idle", last_error="", last_warning="", task_job="", + current_action="", attempt=0, progress={}, end_time=0, + last_heartbeat=time.time()) + return True + + +def clear_all_worker_errors(exclude=()): + """批量清除所有 error/failed 状态(exclude 里的跳过)。返回清除数。""" + exclude = set(exclude) + with _WORKERS_LOCK: + targets = [s for s, w in _WORKERS.items() + if s not in exclude and w.get("status") in ("error", "failed")] + cleared = 0 + for s in targets: + if clear_worker_error(s): + cleared += 1 + return cleared + + def get_worker_heartbeat(serial): """返回 worker 最近一次心跳时间,没有返回 0。""" with _WORKERS_LOCK: diff --git a/static/admin/monitor.js b/static/admin/monitor.js index ca483c1..54dc8a5 100644 --- a/static/admin/monitor.js +++ b/static/admin/monitor.js @@ -130,6 +130,19 @@ async function stopSelectedDevices(){ setTimeout(loadMonitor,500); } +// 清除单台设备的异常状态(error/failed → 未启动) +async function clearDeviceError(serial){ + if(!confirm('确认清除设备 '+serial+' 的异常状态?'))return; + const r=await apiPost('/api/device/clear_error',{serial:serial}); + handleResult(r,()=>loadMonitor()); +} +// 一键清除所有异常/失败设备的错误状态(运行中的任务不受影响) +async function clearAllErrors(){ + if(!confirm('确认清除所有设备的异常状态?(运行中/等待重试的设备会跳过)'))return; + const r=await apiPost('/api/device/clear_all_errors'); + handleResult(r,()=>loadMonitor()); +} + // ================== 设备画面查看 ================== let _shotTimer=null; // 自动刷新定时器 let _shotSerial=''; // 当前查看的 serial @@ -456,10 +469,15 @@ function renderDeviceRow(dev){ const endInfo=(dev.worker_status==='running'&&dev.end_time)?'
预计结束 '+new Date(dev.end_time*1000).toLocaleTimeString()+'
':''; // 选择器健康警告(某步骤连续未找到元素,可能 App 改版选择器失效) const warnInfo=dev.last_warning?'
⚠ '+esc(dev.last_warning)+'
':''; - const stopBtn=(dev.worker_status==='running'||dev.worker_status==='connecting')?'':''; + // 设备操作按钮需要"设备控制"权限(无权限只保留截图查看) + const canDev=_can('devices'); + const stopBtn=canDev&&(dev.worker_status==='running'||dev.worker_status==='connecting')?'':''; // 截图按钮(任何状态都可看,离线时会报错) const shotBtn=''; - const btns='
'+shotBtn+stopBtn+'
'; + // 异常/失败设备提供"清除异常"(把状态恢复为未启动) + const clearBtn=canDev&&(dev.worker_status==='error'||dev.worker_status==='failed') + ?'':''; + const btns='
'+shotBtn+clearBtn+stopBtn+'
'; const devName=dev.device_name||dev.model||''; const devLabel=devName?''+esc(dev.serial)+'
'+esc(devName)+'':''+esc(dev.serial)+''; // 复选框列 diff --git a/templates/admin/monitor.html b/templates/admin/monitor.html index 64883d3..c843f96 100644 --- a/templates/admin/monitor.html +++ b/templates/admin/monitor.html @@ -289,10 +289,11 @@ select.form-control{cursor:pointer}
设备列表
- - - - + + + + + · 每 5 秒自动刷新 · · 未选中 diff --git a/web_server.py b/web_server.py index a0ccfb7..93ad230 100644 --- a/web_server.py +++ b/web_server.py @@ -28,12 +28,13 @@ from flask_login import (LoginManager, login_user, logout_user, from core.stf_client import STFClient from core.task_manager import TaskManager -from core.device_worker import get_all_worker_status +from core.device_worker import clear_worker_error, clear_all_worker_errors from core.logger import get_logger, _LOG_DIR, _MODULE_FILES from core.models import db, init_db, User, DeviceGroup, TaskJob, CustomAction from core.apk_manager import ApkManager -from core.adb_helper import screenshot, list_installed_apps +from core.adb_helper import screenshot, list_installed_apps, adb_connect_light, _ADB_LOCK, _adb from core import uiauto_helper +from config import ADB_PATH, STF_SSH_TARGET, STF_DOCKER_CONTAINER from tasks import list_task_types, get_task_class _log = get_logger("web") @@ -439,19 +440,45 @@ def api_stop_device(): @app.route("/api/stop_all", methods=["POST"]) -@login_required +@perm_required(PERM_DEVICES) def api_stop_all(): stopped = mgr.stop_all() return jsonify({"ok": True, "stopped": stopped}) @app.route("/api/release", methods=["POST"]) -@login_required +@perm_required(PERM_DEVICES) def api_release(): released, failed = stf.release_all_mine() return jsonify({"ok": True, "released": released, "failed": failed}) +@app.route("/api/device/clear_error", methods=["POST"]) +@perm_required(PERM_DEVICES) +def api_device_clear_error(): + """清除单台设备的异常状态(error/failed → idle),供设备列表"清除异常"按钮使用。""" + serial = (request.json or {}).get("serial", "") + if not serial: + return jsonify({"ok": False, "error": "缺少 serial"}), 400 + if serial in mgr.get_running(): + return jsonify({"ok": False, "error": "设备正在运行或等待重试,无法清除"}), 400 + if not clear_worker_error(serial): + return jsonify({"ok": False, "error": "设备正在运行任务,无法清除"}), 400 + _log.info(f"清除设备 {serial} 的异常状态") + return jsonify({"ok": True, "msg": f"已清除 {serial} 的异常状态"}) + + +@app.route("/api/device/clear_all_errors", methods=["POST"]) +@perm_required(PERM_DEVICES) +def api_device_clear_all_errors(): + """一键清除所有异常/失败设备(跳过正在运行/等待重试的)。""" + running = set(mgr.get_running()) + cleared = clear_all_worker_errors(exclude=running) + _log.info(f"一键清除异常:共清除 {cleared} 台") + return jsonify({"ok": True, "cleared": cleared, + "msg": f"已清除 {cleared} 台设备的异常状态"}) + + @app.route("/api/device/screenshot") @login_required def api_device_screenshot():