"""设备池:SQLite devices 表 = 设备清单,本地 adb = 在线状态(STF 替代数据源)。 设计: - list_configured() — devices 表里 enabled 的设备(清单,管理页维护) - list_online() — 本机 `adb devices` 里 state=device 的设备(实时; 池内有 USB 设备时合并 220 远程 adb server 状态) - list_ready() — configured ∩ online(任务调度用) - CRUD — add/update/remove/set_enabled(设备池管理页用) 红线:全模块不 connect / 不 kill-server / 不 disconnect,遵守既有技术约束。 后台线程(task_manager 等)调用时由本模块自行推 app context,调用方无需关心。 """ import time import threading import subprocess 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 _log = get_logger("core.device_pool") _app = None def init_app(app): """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(): """后台线程访问 db 需自行推 app context(与 task_manager._db 同思路)。""" if _app is None: raise RuntimeError("device_pool 未关联 Flask app(web_server 启动时调用 init_app)") return _app.app_context() # ================== 清单 ================== def list_configured(enabled_only=True): """devices 表里的设备清单(serial 列表)。""" with _ctx(): q = Device.query if enabled_only: q = q.filter_by(enabled=True) return [d.serial for d in q.order_by(Device.serial).all()] def list_devices(): """全量设备行(设备池管理页用)。""" with _ctx(): return [d.to_dict() for d in Device.query.order_by(Device.serial).all()] # ================== 在线状态 ================== def list_online(): """本机 adb 中 state=device 的设备 + 池内 USB 设备(经 220 远程 adb server)。 池内无 USB 设备(serial 无冒号)时不查远程 server,零额外开销。 """ out = _adb("devices") or "" serials = [] for line in out.splitlines()[1:]: parts = line.split() if len(parts) >= 2 and parts[0] and parts[1] == "device": serials.append(parts[0]) # 池内有 USB 设备时,补查远程 adb server(220,host 网络 5037 全接口监听) try: usb = [s for s in list_configured() if ":" not in s] except Exception: usb = [] if usb: try: out = _adb_remote(USB_ADB_HOST, USB_ADB_PORT, "devices") or "" for line in out.splitlines()[1:]: parts = line.split() if len(parts) >= 2 and parts[0] and parts[1] == "device": if parts[0] not in serials: serials.append(parts[0]) except Exception: pass return serials def is_online(serial): """设备当前是否在线(本机 adb 可达且状态正常)。""" return serial in list_online() def list_ready(): """可调度设备:清单 ∩ 在线(取代 STF list_free_devices)。""" online = set(list_online()) return [s for s in list_configured() if s in online] # ================== 管理(CRUD) ================== def add_device(serial, name="", note="", enabled=True): """添加/更新设备(upsert)。返回 True 新增 / False 已存在并更新。""" serial = (serial or "").strip() if not serial: return False with _ctx(): d = Device.query.get(serial) if d: d.name, d.note, d.enabled = name or "", note or "", enabled db.session.commit() return False db.session.add(Device(serial=serial, name=name or "", note=note or "", enabled=enabled, created_at=time.strftime("%Y-%m-%d %H:%M"))) db.session.commit() _log.info(f"设备池新增设备: {serial}") return True def remove_device(serial): """删除设备。返回是否删除成功。""" with _ctx(): d = Device.query.get(serial) if not d: return False db.session.delete(d) db.session.commit() _log.info(f"设备池删除设备: {serial}") return True def set_enabled(serial, enabled): """启用/停用设备(停用后不参与调度)。返回是否成功。""" with _ctx(): d = Device.query.get(serial) if not d: return False 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