feat: 正式池断联设备自动重连——①扫描线程每轮顺带对断联的网络设备 adb_connect_light(幂等,连上即恢复,无需人工;此前只有启动时预连接一次,断联后不会自己回来);②断联设备不删不进待连接池(pending 语义=未授权新设备),GET discovery 返回 pool_offline(serial/型号/备注);③POST /api/devices/discovery/reconnect 手动立即重连;④发现面板加「设备池断联设备」区块(自动重连中 + 立即重连按钮)
This commit is contained in:
@@ -22,7 +22,7 @@ from config import (USB_ADB_HOST,
|
|||||||
DISCOVERY_PORT, DISCOVERY_SUBNETS, DISCOVERY_INTERVAL)
|
DISCOVERY_PORT, DISCOVERY_SUBNETS, DISCOVERY_INTERVAL)
|
||||||
from core.adb_helper import _adb, adb_connect_light
|
from core.adb_helper import _adb, adb_connect_light
|
||||||
from core.logger import get_logger
|
from core.logger import get_logger
|
||||||
from core.models import db, PendingDevice
|
from core.models import db, Device, PendingDevice
|
||||||
|
|
||||||
_log = get_logger("core.disc")
|
_log = get_logger("core.disc")
|
||||||
|
|
||||||
@@ -220,8 +220,14 @@ def scan_once(manual=False):
|
|||||||
first_seen=now, last_seen=now))
|
first_seen=now, last_seen=now))
|
||||||
added += 1
|
added += 1
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
# 6. 正式池断联设备自动重连:adb connect 会因 WiFi 波动/设备重启/
|
||||||
|
# adb 服务重启而断开——扫描线程每轮顺带重试(幂等轻量,内部
|
||||||
|
# 全局锁串行),连上即恢复在线,无需人工干预。pending 池是给
|
||||||
|
# 「未授权新设备」的,正式池设备断联不进 pending,而是自动重连。
|
||||||
|
back = _reconnect_offline(configured)
|
||||||
_log.info(f"发现: 探测开放 {len(open_ips)} 台,可连 {len(verified)} 台,"
|
_log.info(f"发现: 探测开放 {len(open_ips)} 台,可连 {len(verified)} 台,"
|
||||||
f"新增待连接 {added} 台")
|
f"新增待连接 {added} 台"
|
||||||
|
+ (f",自动重连恢复 {len(back)} 台 {back}" if back else ""))
|
||||||
_last_scan = (now, len(open_ips), len(verified), added)
|
_last_scan = (now, len(open_ips), len(verified), added)
|
||||||
_last_error = ""
|
_last_error = ""
|
||||||
return True, {"found": len(open_ips), "verified": len(verified), "new": added}
|
return True, {"found": len(open_ips), "verified": len(verified), "new": added}
|
||||||
@@ -240,6 +246,39 @@ def device_pool_list_configured():
|
|||||||
return device_pool.list_configured()
|
return device_pool.list_configured()
|
||||||
|
|
||||||
|
|
||||||
|
# ================== 正式池断联设备:自动重连 ==================
|
||||||
|
def _reconnect_offline(configured):
|
||||||
|
"""对正式池中断联的网络设备逐个 adb 重连,返回恢复的 serial 列表。
|
||||||
|
|
||||||
|
只重连网络设备(IP:5555;USB 设备插着就在,无需 connect)。
|
||||||
|
幂等轻量:内部 adb 全局锁串行,失败静默(下轮扫描再试)。
|
||||||
|
"""
|
||||||
|
online_now = _parse_adb_devices(_adb("devices"))
|
||||||
|
targets = [s for s in (configured or [])
|
||||||
|
if ":" in s and s not in online_now]
|
||||||
|
if not targets:
|
||||||
|
return []
|
||||||
|
for serial in targets:
|
||||||
|
adb_connect_light(serial)
|
||||||
|
online_after = _parse_adb_devices(_adb("devices"))
|
||||||
|
return [s for s in targets if s in online_after]
|
||||||
|
|
||||||
|
|
||||||
|
def list_pool_offline():
|
||||||
|
"""正式设备池中断联的设备(serial + 型号 + 备注名),面板展示用。
|
||||||
|
|
||||||
|
断联设备仍是正式池成员(不删除、不进 pending)——pending 是给未授权
|
||||||
|
新设备的;它们由扫描线程每轮自动重连,也可前端手动立即重连。
|
||||||
|
"""
|
||||||
|
online = _parse_adb_devices(_adb("devices"))
|
||||||
|
with _ctx():
|
||||||
|
rows = [d.to_dict() for d in Device.query.filter_by(enabled=True)
|
||||||
|
.order_by(Device.serial).all()]
|
||||||
|
return [{"serial": r["serial"], "model": r.get("model") or "",
|
||||||
|
"name": r.get("name") or ""}
|
||||||
|
for r in rows if r["serial"] not in online]
|
||||||
|
|
||||||
|
|
||||||
# ================== 定时扫描线程 ==================
|
# ================== 定时扫描线程 ==================
|
||||||
def _discovery_loop():
|
def _discovery_loop():
|
||||||
"""定时扫描 daemon 线程。每轮重读配置(开关/周期即时生效)。
|
"""定时扫描 daemon 线程。每轮重读配置(开关/周期即时生效)。
|
||||||
|
|||||||
@@ -81,6 +81,29 @@ function loadDiscovery(){
|
|||||||
+'<button class="btn btn-xs" data-serial="'+esc(x.serial)+'" onclick="ignoreDiscoveryDev(this.dataset.serial)">忽略</button></td></tr>';
|
+'<button class="btn btn-xs" data-serial="'+esc(x.serial)+'" onclick="ignoreDiscoveryDev(this.dataset.serial)">忽略</button></td></tr>';
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
// 正式池断联设备:仍在设备池(不删除),扫描线程自动重连,可手动立即重连
|
||||||
|
const offTb=document.getElementById('tb-pool-offline');
|
||||||
|
if(offTb){
|
||||||
|
const off=(r.pool_offline||[]);
|
||||||
|
if(!off.length){
|
||||||
|
offTb.innerHTML='<tr><td colspan="5" class="empty">全部在线</td></tr>';
|
||||||
|
}else{
|
||||||
|
offTb.innerHTML=off.map(x=>{
|
||||||
|
return '<tr><td style="font-family:monospace">'+esc(x.serial)+'</td>'
|
||||||
|
+'<td>'+esc(x.model||'-')+'</td><td>'+esc(x.name||'')+'</td>'
|
||||||
|
+'<td><span class="label label-danger">断联·自动重连中</span></td>'
|
||||||
|
+'<td><button class="btn btn-xs" data-serial="'+esc(x.serial)+'" onclick="reconnectPoolDev(this.dataset.serial)">立即重连</button></td></tr>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function reconnectPoolDev(serial){
|
||||||
|
apiPost('/api/devices/discovery/reconnect',{serial}).then(r=>{
|
||||||
|
if(r&&r.ok){
|
||||||
|
showToast('重连已启动(约 5-15 秒生效)','success');
|
||||||
|
setTimeout(loadDiscovery,8000);
|
||||||
|
}else showToast('重连失败: '+((r&&r.error)||''),'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function toggleDiscovery(checked){
|
function toggleDiscovery(checked){
|
||||||
|
|||||||
@@ -766,6 +766,15 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
|
|||||||
</table>
|
</table>
|
||||||
<div class="help">扫描局域网/Tailscale 网段中开放 adb 5555 的设备进入<b>待连接池</b>;
|
<div class="help">扫描局域网/Tailscale 网段中开放 adb 5555 的设备进入<b>待连接池</b>;
|
||||||
<b>确认后才加入正式设备池</b>(不自动连接、不参与任务调度)。未授权设备(手机未接受 RSA 弹窗)不会出现。</div>
|
<b>确认后才加入正式设备池</b>(不自动连接、不参与任务调度)。未授权设备(手机未接受 RSA 弹窗)不会出现。</div>
|
||||||
|
<!-- 正式池断联设备:仍在设备池(不删除、不进待连接池),自动重连中 -->
|
||||||
|
<div style="margin-top:10px">
|
||||||
|
<div style="font-size:12.5px;font-weight:600;margin-bottom:6px">设备池断联设备
|
||||||
|
<span class="text-muted" style="font-weight:400">(仍在设备池内,扫描线程每轮自动重连;也可手动立即重连)</span></div>
|
||||||
|
<table class="table table-hover table-sm">
|
||||||
|
<thead><tr><th>serial</th><th>型号</th><th>备注</th><th>状态</th><th>操作</th></tr></thead>
|
||||||
|
<tbody id="tb-pool-offline"><tr><td colspan="5" class="empty">全部在线</td></tr></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<input type="text" id="devpool-serial" class="form-control" style="width:220px;font-family:monospace"
|
<input type="text" id="devpool-serial" class="form-control" style="width:220px;font-family:monospace"
|
||||||
|
|||||||
+35
-1
@@ -118,14 +118,16 @@ def api_devices_pool_refresh_models():
|
|||||||
@bp.route("/api/devices/discovery", methods=["GET"])
|
@bp.route("/api/devices/discovery", methods=["GET"])
|
||||||
@perm_required(PERM_DEVICES)
|
@perm_required(PERM_DEVICES)
|
||||||
def api_devices_discovery():
|
def api_devices_discovery():
|
||||||
"""发现状态 + 待连接列表(前端 10s 轮询一次请求)。"""
|
"""发现状态 + 待连接列表 + 正式池断联设备(前端 10s 轮询一次请求)。"""
|
||||||
try:
|
try:
|
||||||
status = device_discovery.get_status()
|
status = device_discovery.get_status()
|
||||||
pending = device_discovery.list_pending()
|
pending = device_discovery.list_pending()
|
||||||
|
pool_offline = device_discovery.list_pool_offline()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_log.warning(f"discovery 状态获取失败: {e}")
|
_log.warning(f"discovery 状态获取失败: {e}")
|
||||||
return jsonify({"ok": False, "error": str(e)[:120]}), 503
|
return jsonify({"ok": False, "error": str(e)[:120]}), 503
|
||||||
status["pending"] = pending
|
status["pending"] = pending
|
||||||
|
status["pool_offline"] = pool_offline
|
||||||
return jsonify({"ok": True, **status})
|
return jsonify({"ok": True, **status})
|
||||||
|
|
||||||
|
|
||||||
@@ -182,6 +184,38 @@ def api_devices_discovery_ignore():
|
|||||||
return jsonify({"ok": True, "msg": msg})
|
return jsonify({"ok": True, "msg": msg})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/devices/discovery/reconnect", methods=["POST"])
|
||||||
|
@perm_required(PERM_DEVICES)
|
||||||
|
def api_devices_discovery_reconnect():
|
||||||
|
"""手动立即重连正式池断联设备(后台 adb connect + 采型号)。
|
||||||
|
|
||||||
|
日常无需手动——发现线程每轮(默认 60s)自动重连断联设备;
|
||||||
|
本端点用于不想等下一轮扫描时立即拉起。
|
||||||
|
"""
|
||||||
|
serial = (request.json or {}).get("serial", "").strip()
|
||||||
|
if not serial:
|
||||||
|
return jsonify({"ok": False, "error": "缺少 serial"}), 400
|
||||||
|
from core import device_pool
|
||||||
|
if serial not in device_pool.list_configured():
|
||||||
|
return jsonify({"ok": False, "error": "设备不在设备池中"}), 404
|
||||||
|
|
||||||
|
def _reconnect():
|
||||||
|
try:
|
||||||
|
if ":" in serial:
|
||||||
|
adb_connect(serial)
|
||||||
|
# 无论是否连上,采一次型号(模型失败静默)
|
||||||
|
try:
|
||||||
|
device_pool.refresh_model(serial)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
_log.warning(f"手动重连 {serial} 异常: {e}")
|
||||||
|
|
||||||
|
threading.Thread(target=_reconnect, daemon=True).start()
|
||||||
|
_log.info(f"设备池管理: 手动重连 {serial}")
|
||||||
|
return jsonify({"ok": True, "msg": "重连已启动(约 5-15 秒生效)"})
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/api/devices/discovery/settings", methods=["POST"])
|
@bp.route("/api/devices/discovery/settings", methods=["POST"])
|
||||||
@perm_required(PERM_DEVICES)
|
@perm_required(PERM_DEVICES)
|
||||||
def api_devices_discovery_settings():
|
def api_devices_discovery_settings():
|
||||||
|
|||||||
Reference in New Issue
Block a user