feat: 设备型号采集展示(devices 表 model 列+getprop 采集+管理页型号列/采集按钮+监控页兜底)+ fix: 监控页设备列错位(离线标签残留 td)、appver 端点误删恢复、appver 只查在线设备用轻量连接防挂起
This commit is contained in:
+80
-2
@@ -11,8 +11,10 @@
|
||||
后台线程(task_manager 等)调用时由本模块自行推 app context,调用方无需关心。
|
||||
"""
|
||||
import time
|
||||
import threading
|
||||
import subprocess
|
||||
|
||||
from config import USB_ADB_HOST, USB_ADB_PORT
|
||||
from config import ADB_PATH, USB_ADB_HOST, USB_ADB_PORT
|
||||
from core.adb_helper import _adb, _adb_remote
|
||||
from core.logger import get_logger
|
||||
from core.models import db, Device
|
||||
@@ -23,9 +25,24 @@ _app = None
|
||||
|
||||
|
||||
def init_app(app):
|
||||
"""web_server 启动时调用:绑定 Flask app(供后台线程推 db context)。"""
|
||||
"""web_server 启动时调用:绑定 Flask app(供后台线程推 db context),
|
||||
并后台刷新一次设备型号(首次启动/设备更换后型号可能变化)。"""
|
||||
global _app
|
||||
_app = app
|
||||
try:
|
||||
t = threading.Thread(target=_refresh_models_bg, daemon=True)
|
||||
t.start()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _refresh_models_bg():
|
||||
"""后台批量采集在线设备型号(启动时/手动触发)。失败静默,不影响启动。"""
|
||||
time.sleep(3) # 等服务起来再查
|
||||
try:
|
||||
refresh_all_models()
|
||||
except Exception as e:
|
||||
_log.warning(f"启动型号采集失败(可稍后手动采集): {e}")
|
||||
|
||||
|
||||
def _ctx():
|
||||
@@ -133,3 +150,64 @@ def set_enabled(serial, enabled):
|
||||
d.enabled = enabled
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
|
||||
# ================== 型号采集 ==================
|
||||
def refresh_model(serial, timeout=8):
|
||||
"""采集单台设备型号(getprop ro.product.model)并写回 DB。
|
||||
|
||||
IP:5555 设备走本机 adb;USB 设备先查本机 adb,查不到再查 220 远程 adb server。
|
||||
采集失败返回空串(静默,不影响主流程)。返回采集到的型号。
|
||||
"""
|
||||
if not serial:
|
||||
return ""
|
||||
model = ""
|
||||
if ":" in serial:
|
||||
try:
|
||||
r = subprocess.run([ADB_PATH, "-s", serial, "shell",
|
||||
"getprop", "ro.product.model"],
|
||||
capture_output=True, timeout=timeout)
|
||||
model = (r.stdout or b"").decode("utf-8", errors="replace").strip()
|
||||
except Exception:
|
||||
model = ""
|
||||
else:
|
||||
# USB:先本机,再远程
|
||||
try:
|
||||
r = subprocess.run([ADB_PATH, "-s", serial, "shell",
|
||||
"getprop", "ro.product.model"],
|
||||
capture_output=True, timeout=timeout)
|
||||
model = (r.stdout or b"").decode("utf-8", errors="replace").strip()
|
||||
except Exception:
|
||||
model = ""
|
||||
if not model:
|
||||
model = _adb_remote(USB_ADB_HOST, USB_ADB_PORT, "-s", serial,
|
||||
"shell", "getprop", "ro.product.model")
|
||||
model = model.strip() or ""
|
||||
if not model:
|
||||
return ""
|
||||
with _ctx():
|
||||
d = Device.query.get(serial)
|
||||
if d and d.model != model:
|
||||
d.model = model
|
||||
db.session.commit()
|
||||
_log.info(f"设备池采集型号: {serial} -> {model}")
|
||||
return model
|
||||
|
||||
|
||||
def refresh_all_models():
|
||||
"""批量采集池内在线设备的型号(并发 10,后台线程调用)。返回成功数。"""
|
||||
serials = list_online()
|
||||
if not serials:
|
||||
return 0
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
ok = 0
|
||||
with ThreadPoolExecutor(max_workers=min(10, len(serials))) as pool:
|
||||
futures = {pool.submit(refresh_model, s): s for s in serials}
|
||||
for fut in as_completed(futures):
|
||||
try:
|
||||
if fut.result():
|
||||
ok += 1
|
||||
except Exception:
|
||||
pass
|
||||
_log.info(f"设备池批量采集型号完成: {ok}/{len(serials)} 台")
|
||||
return ok
|
||||
|
||||
+6
-2
@@ -220,17 +220,19 @@ class Device(db.Model):
|
||||
"""设备池(本地设备清单,替代 STF 池作为调度数据源)。
|
||||
|
||||
serial 即 adb 序列号(IP:5555 或 USB 序列号);enabled=False 不参与调度。
|
||||
model 为在线时自动采集的型号(如 Redmi 12C),供管理页/监控页区分设备。
|
||||
"""
|
||||
serial = db.Column(db.String(120), primary_key=True)
|
||||
name = db.Column(db.String(80), default="") # 备注名(可选)
|
||||
model = db.Column(db.String(120), default="") # 型号(自动采集)
|
||||
enabled = db.Column(db.Boolean, default=True) # 是否参与调度
|
||||
note = db.Column(db.Text, default="") # 备注
|
||||
created_at = db.Column(db.String(20), default="") # 添加时间
|
||||
|
||||
def to_dict(self):
|
||||
return {"serial": self.serial, "name": self.name or "",
|
||||
"enabled": bool(self.enabled), "note": self.note or "",
|
||||
"created_at": self.created_at or ""}
|
||||
"model": self.model or "", "enabled": bool(self.enabled),
|
||||
"note": self.note or "", "created_at": self.created_at or ""}
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Device {self.serial}>"
|
||||
@@ -248,6 +250,8 @@ SCHEMA_MIGRATIONS = [
|
||||
"enabled BOOLEAN DEFAULT 1,"
|
||||
"note TEXT DEFAULT '',"
|
||||
"created_at VARCHAR(20) DEFAULT '')"),
|
||||
(3, "设备池:device 表新增 model 列(型号,在线时自动采集)",
|
||||
"ALTER TABLE device ADD COLUMN model TEXT DEFAULT ''"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -824,9 +824,11 @@ class TaskManager:
|
||||
except Exception:
|
||||
online = set()
|
||||
try:
|
||||
names = {d["serial"]: d["name"] for d in device_pool.list_devices()}
|
||||
pool_rows = device_pool.list_devices()
|
||||
names = {d["serial"]: d["name"] for d in pool_rows}
|
||||
models = {d["serial"]: d["model"] for d in pool_rows}
|
||||
except Exception:
|
||||
names = {}
|
||||
names, models = {}, {}
|
||||
|
||||
# 清理陈旧状态:serial 已不在设备池、且没有在跑 worker 的条目,
|
||||
# 避免设备被删除后其失败记录仍残留在"异常汇总"里
|
||||
@@ -845,7 +847,7 @@ class TaskManager:
|
||||
r = running.get(serial, {})
|
||||
result.append({
|
||||
"serial": serial,
|
||||
"model": w.get("model", ""),
|
||||
"model": w.get("model") or models.get(serial, ""),
|
||||
"device_name": names.get(serial, ""),
|
||||
"present": is_online,
|
||||
"ready": is_online, # 阶段 1:ready 概念并入在线状态
|
||||
|
||||
Reference in New Issue
Block a user