feat: 设备身份改为「名称 + 指纹」——更换 IP 自动认领,分组/任务引用自动同步
背景:设备池原先拿 serial(IP)当身份。设备一换 IP(DHCP 重新分配)旧记录就成了连不上的
僵尸条目(表现为"断联·自动重连中"但设备并没关机),分组与 serial 模式的任务还吊着死地址。
2026-09-11 实际发生:.70 变成 .71、.72 消失,平台两个条目永远连不上。
实现
- **设备名称必填且唯一**:加入设备必须填名称;库层面用部分唯一索引兜底
(ux_device_name / ux_device_fingerprint,WHERE 非空 → 兼容历史空值),管理页可改名
- **设备指纹**(ro.serialno):网络设备添加/确认/扫描/采集型号时自动读取;
身份三层拆分——名称(人可读,稳定)、指纹(机器识别,稳定)、serial(当前地址,可变)
- **自动认领**:添加或确认设备时指纹命中池中已有记录 → 迁移原记录到新地址
(名称/型号/备注/启用状态/添加时间全保留),不新增条目
- **人工认领** `POST /api/devices/pool/relocate`:旧地址已断联、指纹没采过时的兜底——
人工指认"这条就是那台,现在在 X",迁移并同步引用
- **引用同步**:认领/迁址时把 device_group.serials 与 task_job.target.serial 的旧地址
换成新地址。⚠️ 必须同时改**内存**:分组/任务在 TaskManager 里另有内存副本且调度用内存对象,
只改库不重启不生效 → device_pool 迁址后回调 TaskManager.sync_device_serial
(装配层用 set_move_hook 注册;device_pool 不能反向 import task_manager,会循环依赖)
- **前端**:待连接池新增「识别」列(指纹命中时提示"≈ 名称(原 IP)",按钮变「认领为 X」);
设备池新增「名称/指纹」列与「改名/换地址」操作;断联设备表也加「换地址」(用户看到断联就在这里)
- 添加设备接口改用 adb_connect_light(单次短超时),避免不可达 IP 让请求卡 30s+;重名校验提前到 adb 之前
文档:API.md §6 重写(设备身份/认领/新接口)、DATA_MODEL.md(新列 + v5/v6 迁移 + 唯一索引)、
ARCHITECTURE.md §4.0(身份三层与引用同步的内存坑)、DEPLOY.md 排查表加"断联但没关机"条目
自测(全通过):名称必填/唯一/改名/重名拒绝(含库层面约束);指纹采集(真实读到 .71 的
gy7lskwkkvj7c6b6);自动认领(指纹命中→迁址+保留名称+带指纹+未命中不误判);人工认领
(迁址+名称保留+分组与任务引用同步);浏览器验证设备池/断联表/待连接池三个界面
This commit is contained in:
@@ -228,18 +228,25 @@ def scan_once(manual=False):
|
||||
# 4. state==device 过滤(排除 unauthorized/offline)
|
||||
verified = _parse_adb_devices(_adb("devices")) & candidates
|
||||
|
||||
# 5. 写 pending:新 → 插入;已有 → 更新 last_seen
|
||||
# 5. 写 pending:新 → 插入;已有 → 更新 last_seen(顺带刷新指纹)
|
||||
# 指纹用于认出"这台其实是设备池里某台设备换了 IP",见 list_pending 的 match
|
||||
now = _fmt()
|
||||
existing = {p.serial for p in PendingDevice.query.all()}
|
||||
added = 0
|
||||
from core import device_pool
|
||||
for serial in verified:
|
||||
source = "tailscale" if serial.split(":")[0].startswith("100.") else "lan"
|
||||
try:
|
||||
fp = device_pool.read_fingerprint(serial, timeout=4)
|
||||
except Exception:
|
||||
fp = ""
|
||||
if serial in existing:
|
||||
PendingDevice.query.filter_by(serial=serial).update(
|
||||
{"last_seen": now})
|
||||
{"last_seen": now, "fingerprint": fp})
|
||||
else:
|
||||
db.session.add(PendingDevice(serial=serial, source=source,
|
||||
first_seen=now, last_seen=now))
|
||||
first_seen=now, last_seen=now,
|
||||
fingerprint=fp))
|
||||
added += 1
|
||||
db.session.commit()
|
||||
# 6. 正式池断联设备自动重连:adb connect 会因 WiFi 波动/设备重启/
|
||||
@@ -371,20 +378,47 @@ def list_pending():
|
||||
with _ctx():
|
||||
rows = [p.to_dict() for p in PendingDevice.query.order_by(
|
||||
PendingDevice.first_seen.desc()).all()]
|
||||
return [dict(r, online=True) for r in rows if r["serial"] in online]
|
||||
out = []
|
||||
for r in rows:
|
||||
if r["serial"] not in online:
|
||||
continue
|
||||
r["online"] = True
|
||||
# 指纹匹配:这台其实就是设备池里某台设备换了地址(前端据此提示"认领")
|
||||
r["match"] = None
|
||||
if r.get("fingerprint"):
|
||||
try:
|
||||
from core import device_pool
|
||||
m = device_pool.find_by_fingerprint(r["fingerprint"])
|
||||
if m and m["serial"] != r["serial"]:
|
||||
r["match"] = {"serial": m["serial"], "name": m.get("name") or ""}
|
||||
except Exception:
|
||||
pass
|
||||
out.append(r)
|
||||
return out
|
||||
|
||||
|
||||
def confirm_pending(serial, name=""):
|
||||
def confirm_pending(serial, name="", fingerprint=""):
|
||||
"""确认连接:pending 行 → 正式设备池(add_device upsert)→ 删 pending。
|
||||
|
||||
返回 (ok, msg, is_new)。adb_connect + 采型号由 API 层后台线程做。
|
||||
先按指纹尝试**认领**:同一台物理设备换了地址时,把池中旧记录迁到新 serial,
|
||||
并同步分组/任务里的引用(名称等信息全部保留),而不是新增一条。
|
||||
|
||||
返回 (ok, msg, is_new)。adb_connect + 采型号/指纹由调用方(API 层后台线程)做。
|
||||
"""
|
||||
with _ctx():
|
||||
row = PendingDevice.query.get(serial)
|
||||
if not row:
|
||||
return False, "设备不在待连接列表", False
|
||||
from core import device_pool
|
||||
is_new = device_pool.add_device(serial, name=name or "")
|
||||
fp = (fingerprint or row.fingerprint or "").strip()
|
||||
claimed_old, claimed_name = device_pool.claim_device(serial, fp)
|
||||
if claimed_old:
|
||||
device_pool.add_device(serial, name=claimed_name or name, fingerprint=fp)
|
||||
PendingDevice.query.filter_by(serial=serial).delete()
|
||||
db.session.commit()
|
||||
return True, (f"已认领为『{claimed_name or claimed_old}』"
|
||||
f"(原地址 {claimed_old},分组/任务的引用已同步)"), False
|
||||
is_new = device_pool.add_device(serial, name=name or "", fingerprint=fp)
|
||||
PendingDevice.query.filter_by(serial=serial).delete()
|
||||
db.session.commit()
|
||||
return True, "已加入设备池" + ("" if is_new else "(已存在,信息已更新)"), is_new
|
||||
|
||||
+196
-5
@@ -22,6 +22,16 @@ from core.models import db, Device
|
||||
_log = get_logger("core.device_pool")
|
||||
|
||||
_app = None
|
||||
# 设备换地址(认领/迁址)后的回调:(old_serial, new_serial) -> None
|
||||
# 由 web_server 装配时注册为 TaskManager.sync_device_serial——
|
||||
# 分组/任务的内存副本在 TaskManager 里,只改库不改内存不生效(见 _move_device_row)
|
||||
_move_hook = None
|
||||
|
||||
|
||||
def set_move_hook(fn):
|
||||
"""注册"设备换地址"回调(web_server 创建完 TaskManager 后调用)。"""
|
||||
global _move_hook
|
||||
_move_hook = fn
|
||||
|
||||
|
||||
def init_app(app):
|
||||
@@ -109,8 +119,131 @@ def list_ready():
|
||||
return [s for s in list_configured() if s in online]
|
||||
|
||||
|
||||
# ================== 设备指纹(识别"同一台物理设备") ==================
|
||||
def read_fingerprint(serial, timeout=6):
|
||||
"""读取设备指纹(ro.serialno)——设备换 IP 后据此认领回原记录。
|
||||
|
||||
只对网络设备(serial 含 ":")读取:USB 设备的 serial 本身就是稳定序列号,
|
||||
不存在"换地址"问题。采集失败返回空串(静默,不影响主流程)。
|
||||
"""
|
||||
serial = (serial or "").strip()
|
||||
if not serial or ":" not in serial:
|
||||
return ""
|
||||
# 部分机型 ro.serialno 为空,退回 ro.boot.serialno
|
||||
for prop in ("ro.serialno", "ro.boot.serialno"):
|
||||
try:
|
||||
r = subprocess.run([ADB_PATH, "-s", serial, "shell", "getprop", prop],
|
||||
capture_output=True, timeout=timeout)
|
||||
fp = (r.stdout or b"").decode("utf-8", errors="replace").strip()
|
||||
except Exception:
|
||||
continue
|
||||
if fp and fp.isprintable():
|
||||
return fp
|
||||
return ""
|
||||
|
||||
|
||||
def find_by_fingerprint(fingerprint, exclude_serial=""):
|
||||
"""按指纹查池中设备(返回 dict 或 None),可排除指定 serial。"""
|
||||
fingerprint = (fingerprint or "").strip()
|
||||
if not fingerprint:
|
||||
return None
|
||||
with _ctx():
|
||||
row = Device.query.filter(Device.fingerprint == fingerprint).first()
|
||||
if not row or row.serial == exclude_serial:
|
||||
return None
|
||||
return row.to_dict()
|
||||
|
||||
|
||||
def name_taken(name, exclude_serial=""):
|
||||
"""名称是否已被其它设备占用(唯一约束的应用层检查,给友好提示用)。"""
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
return False
|
||||
with _ctx():
|
||||
q = Device.query.filter(Device.name == name)
|
||||
if exclude_serial:
|
||||
q = q.filter(Device.serial != exclude_serial)
|
||||
return q.first() is not None
|
||||
|
||||
|
||||
def _move_device_row(old, new_serial, fingerprint=""):
|
||||
"""把 old 记录迁到 new_serial(调用方持 app context)。
|
||||
|
||||
只动 device 表本身;**分组/任务引用的同步交给上层**(见 set_move_hook):
|
||||
分组与任务在 TaskManager 里还有一份内存副本,调度用的是内存对象——
|
||||
只改库不改内存,不重启不生效。而 device_pool 不能反向依赖 task_manager
|
||||
(会造成循环 import),所以用回调把这件事交给装配层。
|
||||
"""
|
||||
old_serial, name = old.serial, (old.name or "")
|
||||
# 新地址上若已有记录(重复添加等),以"被认领的旧记录"为准,删掉它
|
||||
dup = Device.query.get(new_serial)
|
||||
if dup is not None and dup.serial != old.serial:
|
||||
db.session.delete(dup)
|
||||
old.serial = new_serial
|
||||
if fingerprint:
|
||||
old.fingerprint = fingerprint
|
||||
db.session.commit()
|
||||
if _move_hook is not None:
|
||||
try:
|
||||
_move_hook(old_serial, new_serial)
|
||||
except Exception as e:
|
||||
_log.warning(f"设备迁址后同步分组/任务引用失败: {e}")
|
||||
return old_serial, name
|
||||
|
||||
|
||||
def claim_device(new_serial, fingerprint):
|
||||
"""认领:同一台物理设备换了地址,把池中旧记录迁到新 serial 并同步所有引用。
|
||||
|
||||
做三件事(一个事务内):
|
||||
1. 把旧记录的 serial 改成新地址(名称/型号/备注/启用状态/添加时间全部保留)
|
||||
2. 同步 device_group.serials 里的旧 serial → 新 serial(否则分组吊着死 IP)
|
||||
3. 同步 task_job.target.serial(指定设备模式的任务目标)
|
||||
|
||||
返回 (old_serial, name);没有匹配到旧记录时返回 (None, "")。
|
||||
"""
|
||||
fingerprint = (fingerprint or "").strip()
|
||||
new_serial = (new_serial or "").strip()
|
||||
if not fingerprint or not new_serial:
|
||||
return None, ""
|
||||
with _ctx():
|
||||
old = Device.query.filter(Device.fingerprint == fingerprint,
|
||||
Device.serial != new_serial).first()
|
||||
if not old:
|
||||
return None, ""
|
||||
old_serial, name = _move_device_row(old, new_serial)
|
||||
_log.info(f"设备认领: 指纹 {fingerprint} 的『{name or old_serial}』"
|
||||
f"由 {old_serial} 迁到 {new_serial}")
|
||||
return old_serial, name
|
||||
|
||||
|
||||
def relocate_device(old_serial, new_serial, fingerprint=""):
|
||||
"""人工认领:把池中 old_serial 的记录改到 new_serial(换地址的手工兜底)。
|
||||
|
||||
用在"设备已经断联、读不到指纹"的场景:设备换了 IP 后旧地址连不上,
|
||||
指纹也没采过,自动认领无从匹配——此时由人工指认"这条就是那台,
|
||||
现在在 X",本函数负责迁移并同步分组/任务引用。
|
||||
|
||||
返回 (ok, name, msg)。
|
||||
"""
|
||||
old_serial = (old_serial or "").strip()
|
||||
new_serial = (new_serial or "").strip()
|
||||
if not old_serial or not new_serial:
|
||||
return False, "", "缺少参数"
|
||||
if old_serial == new_serial:
|
||||
return False, "", "新旧地址相同,无需迁移"
|
||||
with _ctx():
|
||||
old = Device.query.get(old_serial)
|
||||
if not old:
|
||||
return False, "", f"设备池中没有 {old_serial}"
|
||||
if Device.query.get(new_serial) is not None:
|
||||
return False, "", f"{new_serial} 已在设备池中,请先处理那条记录"
|
||||
_, name = _move_device_row(old, new_serial, fingerprint)
|
||||
_log.info(f"设备人工认领: 『{name or old_serial}』{old_serial} → {new_serial}")
|
||||
return True, name, f"『{name or old_serial}』已迁到 {new_serial},分组/任务引用已同步"
|
||||
|
||||
|
||||
# ================== 管理(CRUD) ==================
|
||||
def add_device(serial, name="", note="", enabled=True):
|
||||
def add_device(serial, name="", note="", enabled=True, fingerprint=""):
|
||||
"""添加/更新设备(upsert)。返回 True 新增 / False 已存在并更新。"""
|
||||
serial = (serial or "").strip()
|
||||
if not serial:
|
||||
@@ -119,16 +252,47 @@ def add_device(serial, name="", note="", enabled=True):
|
||||
d = Device.query.get(serial)
|
||||
if d:
|
||||
d.name, d.note, d.enabled = name or "", note or "", enabled
|
||||
if fingerprint:
|
||||
d.fingerprint = fingerprint
|
||||
db.session.commit()
|
||||
return False
|
||||
db.session.add(Device(serial=serial, name=name or "", note=note or "",
|
||||
enabled=enabled,
|
||||
enabled=enabled, fingerprint=fingerprint or "",
|
||||
created_at=time.strftime("%Y-%m-%d %H:%M")))
|
||||
db.session.commit()
|
||||
_log.info(f"设备池新增设备: {serial}")
|
||||
return True
|
||||
|
||||
|
||||
def rename_device(serial, name):
|
||||
"""重命名设备(唯一性由调用方先校验)。返回是否成功。"""
|
||||
name = (name or "").strip()
|
||||
with _ctx():
|
||||
d = Device.query.get(serial)
|
||||
if not d or not name or (d.name or "") == name:
|
||||
return False
|
||||
old = d.name or ""
|
||||
d.name = name
|
||||
db.session.commit()
|
||||
_log.info(f"设备池重命名: {old or serial} → {name}({serial})")
|
||||
return True
|
||||
|
||||
|
||||
def set_fingerprint(serial, fingerprint):
|
||||
"""补写设备指纹(认领需要;采集是后来才做的)。"""
|
||||
fingerprint = (fingerprint or "").strip()
|
||||
if not fingerprint:
|
||||
return False
|
||||
with _ctx():
|
||||
d = Device.query.get(serial)
|
||||
if not d or d.fingerprint == fingerprint:
|
||||
return False
|
||||
d.fingerprint = fingerprint
|
||||
db.session.commit()
|
||||
_log.info(f"设备池采集指纹: {serial} -> {fingerprint}")
|
||||
return True
|
||||
|
||||
|
||||
def remove_device(serial):
|
||||
"""删除设备。返回是否删除成功。"""
|
||||
with _ctx():
|
||||
@@ -194,18 +358,45 @@ def refresh_model(serial, timeout=8):
|
||||
return model
|
||||
|
||||
|
||||
def refresh_fingerprint(serial, timeout=6):
|
||||
"""读取并写回设备指纹,返回指纹(失败返回空串)。"""
|
||||
fp = read_fingerprint(serial, timeout=timeout)
|
||||
if fp:
|
||||
set_fingerprint(serial, fp)
|
||||
return fp
|
||||
|
||||
|
||||
def refresh_info(serial):
|
||||
"""采集一台设备的型号 + 指纹(指纹缺失时才读,避免每次启动都白跑一次 adb)。
|
||||
|
||||
返回 (model, fingerprint)。任一失败都静默——设备池展示用,不影响主流程。
|
||||
"""
|
||||
model = refresh_model(serial)
|
||||
fp = ""
|
||||
with _ctx():
|
||||
row = Device.query.get(serial)
|
||||
fp = (row.fingerprint or "") if row else ""
|
||||
if not fp:
|
||||
fp = refresh_fingerprint(serial)
|
||||
return model, fp
|
||||
|
||||
|
||||
def refresh_all_models():
|
||||
"""批量采集池内在线设备的型号(并发 10,后台线程调用)。返回成功数。"""
|
||||
"""批量采集池内在线设备的型号(顺带补齐缺失的设备指纹),并发 10,后台线程调用。
|
||||
|
||||
返回成功数。指纹补齐很重要:老库里的设备没有指纹,补上之后换 IP 才能被认领。
|
||||
"""
|
||||
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}
|
||||
futures = {pool.submit(refresh_info, s): s for s in serials}
|
||||
for fut in as_completed(futures):
|
||||
try:
|
||||
if fut.result():
|
||||
model, _fp = fut.result()
|
||||
if model:
|
||||
ok += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
+46
-7
@@ -223,16 +223,20 @@ class Device(db.Model):
|
||||
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="") # 添加时间
|
||||
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="") # 添加时间
|
||||
# 设备指纹(ro.serialno):识别"同一台物理设备"的稳定标识。
|
||||
# 网络设备(serial=IP:5555)换 IP 后靠它认领回原记录,名称/分组/任务引用都不丢。
|
||||
fingerprint = db.Column(db.String(120), default="")
|
||||
|
||||
def to_dict(self):
|
||||
return {"serial": self.serial, "name": self.name or "",
|
||||
"model": self.model or "", "enabled": bool(self.enabled),
|
||||
"note": self.note or "", "created_at": self.created_at or ""}
|
||||
"note": self.note or "", "created_at": self.created_at or "",
|
||||
"fingerprint": self.fingerprint or ""}
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Device {self.serial}>"
|
||||
@@ -247,10 +251,14 @@ class PendingDevice(db.Model):
|
||||
source = db.Column(db.String(20), default="") # lan / tailscale
|
||||
first_seen = db.Column(db.String(20), default="") # 首次发现时间
|
||||
last_seen = db.Column(db.String(20), default="") # 最近一次扫描仍可见的时间
|
||||
# 扫描时顺带读取的设备指纹:与设备池中已有记录比对,用于提示
|
||||
# 「这台其实就是 <名称>(原 IP 变了)」而不是让用户在一堆陌生 IP 里猜
|
||||
fingerprint = db.Column(db.String(120), default="")
|
||||
|
||||
def to_dict(self):
|
||||
return {"serial": self.serial, "source": self.source or "",
|
||||
"first_seen": self.first_seen or "", "last_seen": self.last_seen or ""}
|
||||
"first_seen": self.first_seen or "", "last_seen": self.last_seen or "",
|
||||
"fingerprint": self.fingerprint or ""}
|
||||
|
||||
|
||||
# 版本化 schema 迁移:新增结构变更时在此追加 (版本号, 说明, SQL)
|
||||
@@ -273,8 +281,22 @@ SCHEMA_MIGRATIONS = [
|
||||
"source VARCHAR(20) DEFAULT '',"
|
||||
"first_seen VARCHAR(20) DEFAULT '',"
|
||||
"last_seen VARCHAR(20) DEFAULT '')"),
|
||||
(5, "设备池:device 表新增 fingerprint 列(设备指纹 ro.serialno,换 IP 后认领回原记录)",
|
||||
"ALTER TABLE device ADD COLUMN fingerprint VARCHAR(120) DEFAULT ''"),
|
||||
(6, "自动发现:pending_device 表新增 fingerprint 列(扫描时读取,用于提示是已有设备换了 IP)",
|
||||
"ALTER TABLE pending_device ADD COLUMN fingerprint VARCHAR(120) DEFAULT ''"),
|
||||
]
|
||||
|
||||
# 唯一索引(部分索引:空值不参与唯一约束,兼容历史未命名/未采指纹的老数据)
|
||||
# 名称唯一 = 设备的人可读标识;指纹唯一 = 一台物理设备在池中只能有一条记录
|
||||
_UNIQUE_INDEXES = (
|
||||
("ux_device_name", "CREATE UNIQUE INDEX IF NOT EXISTS ux_device_name "
|
||||
"ON device(name) WHERE name IS NOT NULL AND name <> ''"),
|
||||
("ux_device_fingerprint", "CREATE UNIQUE INDEX IF NOT EXISTS ux_device_fingerprint "
|
||||
"ON device(fingerprint) WHERE fingerprint IS NOT NULL "
|
||||
"AND fingerprint <> ''"),
|
||||
)
|
||||
|
||||
|
||||
def init_db(app):
|
||||
"""在 Flask app context 里初始化数据库 + 创建默认管理员。
|
||||
@@ -322,10 +344,27 @@ def _migrate_schema():
|
||||
{"v": str(version)})
|
||||
db.session.commit()
|
||||
_log.info(f"schema 迁移到版本 {version}: {desc}")
|
||||
_ensure_unique_indexes()
|
||||
except Exception as e:
|
||||
_log.error(f"schema 迁移失败(不阻塞启动): {e}")
|
||||
|
||||
|
||||
def _ensure_unique_indexes():
|
||||
"""建设备池的唯一索引(幂等)。
|
||||
|
||||
单独抽出来是因为索引不属于某个版本迁移:老库升级后也要补建。
|
||||
历史数据若存在重复(名称/指纹撞车),建索引会失败——只告警不回滚、
|
||||
不阻塞启动,由管理页提示用户改名(唯一约束从此刻起对新数据生效)。
|
||||
"""
|
||||
for name, sql in _UNIQUE_INDEXES:
|
||||
try:
|
||||
db.session.execute(text(sql))
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
_log.warning(f"唯一索引 {name} 创建失败(历史数据可能有重复): {e}")
|
||||
|
||||
|
||||
def _ensure_default_admin():
|
||||
"""首次启动创建默认管理员 admin/admin123。"""
|
||||
if not User.query.filter_by(username="admin").first():
|
||||
|
||||
@@ -449,6 +449,40 @@ class TaskManager:
|
||||
self._save_groups()
|
||||
return g
|
||||
|
||||
def sync_device_serial(self, old_serial, new_serial):
|
||||
"""设备换地址(认领/迁址)后,把分组与任务里的引用改到新地址。
|
||||
|
||||
由 device_pool 的迁址回调触发(见 device_pool.set_move_hook)。
|
||||
**必须同时改内存与库**:调度用的是内存里的 groups/jobs,只改库不重启不生效
|
||||
—— 表现就是"分组里少了一台、指定设备的任务仍跑向不存在的地址"。
|
||||
|
||||
返回被更新的引用条数。
|
||||
"""
|
||||
if not old_serial or not new_serial or old_serial == new_serial:
|
||||
return 0
|
||||
n = 0
|
||||
for name, g in list(self.groups.items()):
|
||||
try:
|
||||
serials = list(g.serials or [])
|
||||
except Exception:
|
||||
continue
|
||||
if old_serial in serials:
|
||||
self.update_group(name, serials=[new_serial if s == old_serial else s
|
||||
for s in serials])
|
||||
n += 1
|
||||
_log.info(f"设备换地址: 分组『{name}』的 {old_serial} → {new_serial}")
|
||||
for job_id, job in list(self.jobs.items()):
|
||||
try:
|
||||
target = dict(job.target or {})
|
||||
except Exception:
|
||||
continue
|
||||
if target.get("mode") == "serial" and target.get("serial") == old_serial:
|
||||
target["serial"] = new_serial
|
||||
self.update_job(job_id, target=target)
|
||||
n += 1
|
||||
_log.info(f"设备换地址: 任务『{job.name}』目标 → {new_serial}")
|
||||
return n
|
||||
|
||||
def delete_group(self, name):
|
||||
if name in self.groups:
|
||||
del self.groups[name]
|
||||
|
||||
Reference in New Issue
Block a user